summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLorry Tar Creator <lorry-tar-importer@lorry>2015-06-01 14:15:30 +0000
committerLorry Tar Creator <lorry-tar-importer@lorry>2015-06-01 14:15:30 +0000
commit1425eea04dd872dc6313f5315f317b2de288037c (patch)
treef81c74f75429e829714029850f89ee4c7f13aa39
downloadIO-Async-tarball-master.tar.gz
-rw-r--r--Build.PL50
-rw-r--r--Changes844
-rw-r--r--LICENSE379
-rw-r--r--MANIFEST113
-rw-r--r--META.json195
-rw-r--r--META.yml139
-rw-r--r--Makefile.PL23
-rw-r--r--README278
-rw-r--r--examples/chat-server.pl71
-rw-r--r--examples/echo-server.pl69
-rw-r--r--examples/netcat-client.pl68
-rw-r--r--examples/readwrite-futures.pl17
-rw-r--r--examples/tail-logfile.pl32
-rw-r--r--examples/tcp-proxy.pl81
-rw-r--r--examples/whoami-server.pl58
-rw-r--r--lib/IO/Async.pm366
-rw-r--r--lib/IO/Async/Channel.pm471
-rw-r--r--lib/IO/Async/ChildManager.pm705
-rw-r--r--lib/IO/Async/Debug.pm98
-rw-r--r--lib/IO/Async/File.pm219
-rw-r--r--lib/IO/Async/FileStream.pm413
-rw-r--r--lib/IO/Async/Function.pm667
-rw-r--r--lib/IO/Async/Future.pm150
-rw-r--r--lib/IO/Async/Handle.pm687
-rw-r--r--lib/IO/Async/Internals/Connector.pm243
-rw-r--r--lib/IO/Async/Internals/TimeQueue.pm205
-rw-r--r--lib/IO/Async/Listener.pm549
-rw-r--r--lib/IO/Async/Loop.pm2781
-rw-r--r--lib/IO/Async/Loop/Poll.pm350
-rw-r--r--lib/IO/Async/Loop/Select.pm294
-rw-r--r--lib/IO/Async/LoopTests.pm833
-rw-r--r--lib/IO/Async/Notifier.pm919
-rw-r--r--lib/IO/Async/OS.pm599
-rw-r--r--lib/IO/Async/OS/MSWin32.pm111
-rw-r--r--lib/IO/Async/OS/cygwin.pm40
-rw-r--r--lib/IO/Async/OS/linux.pm59
-rw-r--r--lib/IO/Async/PID.pm196
-rw-r--r--lib/IO/Async/Process.pm849
-rw-r--r--lib/IO/Async/Protocol.pm259
-rw-r--r--lib/IO/Async/Protocol/LineStream.pm138
-rw-r--r--lib/IO/Async/Protocol/Stream.pm237
-rw-r--r--lib/IO/Async/Resolver.pm689
-rw-r--r--lib/IO/Async/Routine.pm436
-rw-r--r--lib/IO/Async/Signal.pm150
-rw-r--r--lib/IO/Async/Socket.pm358
-rw-r--r--lib/IO/Async/Stream.pm1419
-rw-r--r--lib/IO/Async/Test.pm185
-rw-r--r--lib/IO/Async/Timer.pm187
-rw-r--r--lib/IO/Async/Timer/Absolute.pm142
-rw-r--r--lib/IO/Async/Timer/Countdown.pm274
-rw-r--r--lib/IO/Async/Timer/Periodic.pm249
-rw-r--r--t/00use.t35
-rw-r--r--t/01timequeue.t94
-rw-r--r--t/02os.t170
-rw-r--r--t/03loop-magic.t51
-rw-r--r--t/04notifier.t194
-rw-r--r--t/05notifier-loop.t129
-rw-r--r--t/06notifier-mixin.t50
-rw-r--r--t/07notifier-future.t58
-rw-r--r--t/10loop-poll-io.t7
-rw-r--r--t/10loop-select-io.t7
-rw-r--r--t/11loop-poll-timer.t7
-rw-r--r--t/11loop-select-timer.t7
-rw-r--r--t/12loop-poll-signal.t9
-rw-r--r--t/12loop-select-signal.t9
-rw-r--r--t/13loop-poll-idle.t7
-rw-r--r--t/13loop-select-idle.t7
-rw-r--r--t/14loop-poll-child.t7
-rw-r--r--t/14loop-select-child.t7
-rw-r--r--t/15loop-poll-control.t7
-rw-r--r--t/15loop-select-control.t7
-rw-r--r--t/18loop-poll-legacy.t95
-rw-r--r--t/18loop-select-legacy.t152
-rw-r--r--t/19loop-future.t107
-rw-r--r--t/19test.t69
-rw-r--r--t/20handle.t422
-rw-r--r--t/21stream-1read.t637
-rw-r--r--t/21stream-2write.t479
-rw-r--r--t/21stream-3split.t187
-rw-r--r--t/21stream-4encoding.t151
-rw-r--r--t/22timer-absolute.t143
-rw-r--r--t/22timer-countdown.t257
-rw-r--r--t/22timer-periodic.t233
-rw-r--r--t/23signal.t148
-rw-r--r--t/24listener.t301
-rw-r--r--t/25socket.t325
-rw-r--r--t/26pid.t89
-rw-r--r--t/27file.t113
-rw-r--r--t/28filestream.t323
-rw-r--r--t/30loop-fork.t81
-rw-r--r--t/31loop-spawnchild.t168
-rw-r--r--t/32loop-spawnchild-setup.t439
-rw-r--r--t/33process.t245
-rw-r--r--t/34process-handles.t429
-rw-r--r--t/35loop-openchild.t65
-rw-r--r--t/36loop-runchild.t158
-rw-r--r--t/37loop-child-root.t89
-rw-r--r--t/38loop-thread.t59
-rw-r--r--t/40channel.t263
-rw-r--r--t/41routine.t322
-rw-r--r--t/42function.t569
-rw-r--r--t/50resolver.t389
-rw-r--r--t/51loop-connect.t333
-rw-r--r--t/52loop-listen.t183
-rw-r--r--t/53loop-extend.t103
-rw-r--r--t/60protocol.t146
-rw-r--r--t/61protocol-stream.t245
-rw-r--r--t/62protocol-linestream.t118
-rw-r--r--t/63handle-connect.t84
-rw-r--r--t/64handle-bind.t36
-rw-r--r--t/99pod.t11
-rw-r--r--t/StupidLoop.pm8
-rw-r--r--t/TimeAbout.pm31
113 files changed, 28618 insertions, 0 deletions
diff --git a/Build.PL b/Build.PL
new file mode 100644
index 0000000..ddb9c7a
--- /dev/null
+++ b/Build.PL
@@ -0,0 +1,50 @@
+use strict;
+use warnings;
+
+use Module::Build;
+
+my $build = Module::Build->new(
+ module_name => 'IO::Async',
+ requires => {
+ 'Future' => '0.26', # ->is_failed
+ 'Future::Utils' => '0.18', # try_repeat
+ 'Exporter' => '5.57',
+ 'File::stat' => 0,
+ 'IO::Poll' => 0,
+ 'Socket' => '2.007',
+ 'Storable' => 0,
+ 'Struct::Dumb' => 0,
+ 'Time::HiRes' => 0,
+
+ # Fails on perl 5.8.3 for unknown reasons
+ # https://rt.cpan.org/Ticket/Display.html?id=64493
+ # Now 5.16 is stable, I see even less reason to worry about such an old Perl
+
+ # Furthermore we've started using 5.10'isms
+ 'perl' => '5.010',
+ },
+ recommends => {
+ 'IO::Socket::IP' => 0,
+ },
+ test_requires => {
+ 'File::Temp' => 0,
+ 'Test::Fatal' => 0,
+ 'Test::Identity' => 0,
+ 'Test::More' => '0.88',
+ 'Test::Refcount' => 0,
+ },
+ configure_requires => {
+ 'Module::Build' => '0.4004', # test_requires
+ },
+ license => 'perl',
+ create_makefile_pl => 'traditional',
+ create_license => 1,
+ create_readme => 1,
+ meta_merge => {
+ resources => {
+ x_IRC => "irc://irc.perl.org/#io-async",
+ },
+ },
+);
+
+$build->create_build_script;
diff --git a/Changes b/Changes
new file mode 100644
index 0000000..8fc2d7f
--- /dev/null
+++ b/Changes
@@ -0,0 +1,844 @@
+Revision history for IO-Async
+
+0.67 2015/06/01 15:06:13
+ [CHANGES]
+ * Add a ->post_fork method to IO::Async::Loop in case subclasses
+ should take specific action (RT104130)
+ * Remove IO::Async::MergePoint entirely
+ * Add debug_printf() calls to IO::Async::Process
+ * Various documentation additions
+
+ [BUGFIXES]
+ * Remember to actually delete unused filehandles from the pollmask
+ (RT103922)
+
+0.66 2015/04/17 20:36:39
+ [CHANGES]
+ * Created IO::Async::Debug
+ * Ignore SIGPIPE by default (RT92024)
+ * IaSocket->bind now returns a Future
+ * Added IaSocket->bind resolver support and neatened up UDP examples
+ in synopsis/documentation
+
+ [BUGFIXES]
+ * Correct call to unpack() in IaStream example (RT103143)
+ * Don't silently eat accept() failures in IaListener (RT102677)
+ * Remember to add the new resolver instance to $loop in
+ ->set_resolver (RT103446)
+ * Correct implementation of ->unwatch_io on IaLoop::Poll to prevent
+ 100% CPU spin (RT103445)
+ * Ensure that an exception thrown by Timer::Periodic's on_tick
+ doesn't prevent rescheduling
+
+0.65 2015/02/15 14:28:02
+ [CHANGES]
+ * Optionally allow IO::Async::Channel to use 'Sereal' serialisation
+ * Added documentation about the 'env' Child Manager key and copying
+ %ENV
+ * Take OS "preferred loop subclass" hints from IO::Async::OS directly
+
+ [BUGFIXES]
+ * Nested $stream->read_* inside read_* cause double-completion of
+ Future (RT101774)
+ * Implement IO::Async::Loop::Poll directly on _poll() syscall
+ wrapper, thus avoiding many bugs in IO::Poll (RT93141)
+ * Ensure that IO::Async::Loop::Select can cope with callbacks that
+ remove other IO handle watches (RT101919)
+ * Silently upgrade watched IO handles to O_NONBLOCK (RT102044)
+ * Complain about unrecognised keys to ->extract_addrinfo (RT101453)
+
+0.64 2014/10/17 17:51:07
+ [CHANGES]
+ * Make specific mention of 'TCP' and 'UDP' around socket examples
+ where appropriate
+ * Allow construction of an IO::Async::Handle using fileno integers
+ directly
+ * Provide a better search for 'all open filehandles' via IO::Async::OS
+ on Linux (RT97942)
+ * Allow IO::Async::Listener to have handle_constructor or handle_class
+ as a subclass method (RT97208)
+ * Clarify documentation on how to use IO::Async::Process's
+ on_exception event (RT98929)
+
+ [BUGFIXES]
+ * Ensure that Stream's write Futures are also informed of write errors
+ (RT97433)
+ * Remember to ->remove_child the individual workers of an
+ IO::Async::Function (RT99552)
+ * Fix IO::Async::Function synopsis example (RT97713)
+
+0.63 2014/07/11 15:09:08
+ [CHANGES]
+ * Allow Notifier subclasses to last-ditch handle unrecognised
+ ->configure() params
+ * Added $notifier->adopt_future
+ * Added $notifier->invoke_error and 'on_error' event
+ * Ensure that TimeQueue inserts in FIFO order for equal timestamps
+ * Kill remaining docs to long-dead IO::Async::Sequencer
+
+ [BUGFIXES]
+ * Cygwin needs the SELECT_CONNECT_EVEC OS hint as well
+ * Probe for a broken port to perform listen() tests on by using
+ ReuseAddr => 1 so it matches what IO::Async will do (RT84051)
+
+0.62 2014/03/27 23:15:25
+ [CHANGES]
+ * Added IO::Async::Future->{done,fail}_later
+ * Allow overriding of debug log file or file descriptor
+ * Avoid Future's and_then/or_else methods
+ * Allow Channel->recv in async mode to return a Future (RT91180)
+ * Ensure that Function ->call Futures cancel correctly
+ * Added $routine->kill
+ * Kill the 'getaddrinfo' => 'getaddrinfo_array' legacy redirection
+ * Allow Loop's resolver to be changed to a different object
+
+ [BUGFIXES]
+ * Avoid relying on strong forward references in Future, by creating
+ intentional cycles on pending Futures. Workaround for bugfix in
+ upcoming Future release.
+
+0.61 2013/10/15 01:10:51
+ [CHANGES]
+ * Some OSes lack signals; forbid the ->*_signal methods, and use
+ waitpid() polling for child processes if so
+ * Rearrangement of $loop->listen and IO::Async::Listener logic to
+ allow Listener subclasses to use listen extensions (e.g. SSL)
+ * Allow ->listen to construct new Stream or Socket handles
+ * Updated documentation and tests to emphasise futures with resolver
+ and ->listen
+ * Support spawning threads and watching for exit
+ * Support IO::Async::Routine based on threads
+ * Various MSWin32 improvements and fixes - it now passes tests \o/
+ * Declare that MSWin32 does not support POSIX-like fork(); skip all
+ unit tests and functionallity based on it if so
+
+ Note: These changes break IO::Async::SSL versions 0.12 or older.
+
+ [BUGFIXES]
+ * Ensure that $stream->write( CODE, on_write/write_len ) works
+ correctly
+
+ With many thanks to Mithaldu for the use of his Windows smoker for
+ development and testing of the MSWin32 fixes
+
+0.60 2013/09/19 14:26:22
+ [CHANGES]
+ * Updated for Future 0.16 - no longer needs 'return' argument for
+ Future::Utils functions
+ * $stream->connect() ought to default socktype => "stream"
+
+ [BUGFIXES]
+ * Fix unit tests to better handle INADDR_LOOPBACK not being 127.0.0.1
+ * Skip-guard ->socket("inet6") unit tests on machines unable to
+ socket(AF_INET6)
+ * Remmeber to ->accept connections to testing socket in
+ t/63handle-connect.t
+
+0.59 CHANGES:
+ * Allow IO::Async::Stream to define custom reader/writer methods
+ * Support writeready-for-read and readready-for-write in Stream
+ * Allow Stream->write() on_write and write_len args
+ * Neatened and documented Future ->fail arguments and conventions
+ * Added Stream on_writeable_{start,stop} events
+ * Added Handle->socket, ->bind and ->connect methods
+ * Revamp of Loop->connect logic; allow passing through an
+ IO::Async::Handle instance
+
+ BUGFIXES:
+ * Ensure that stream read EOF state is visible during queued on_read
+ events that caused it
+ * Fix 'return ... or die' precendence bug in Resolver (RT87051)
+ * Need to poll() for POLLPRI on MSWin32 and Cygwin
+
+0.58 CHANGES:
+ * Added Stream read watermarks
+
+ BUGFIXES:
+ * Fix weakself event handlers' use of "shift or return"
+
+0.57 CHANGES:
+ * Allow Stream->write from a Future, CODE that returns Future, Future
+ that returns CODE, etc...
+ * Added Future-returning Stream->read_* methods and ->push_on_read
+ * Return a flush-complete notification Future from Stream->write
+ * Allow Timer::Periodic to stop itself from its own on_tick event
+ * Wrap transport on_{read,write}_eof from Protocol::Stream
+
+0.56 CHANGES:
+ * Added $loop->delay_future and $loop->timeout_future
+ * Added $future->loop accessor
+ * Use a faster splice()-based mechanism for the ARRAY-based TimeQueue
+ * Updated for Future::Utils 0.12 'repeat' function
+
+ BUGFIXES:
+ * Ensure that Process from => "" works
+ * If select() returns -1 ignore the bit vectors
+ * pipe() on Windows doesn't play with select(); emulate ->pipepair
+ using ->socketpair
+ * Correct use of S_ISREG and stat()
+
+0.55 CHANGES:
+ * Try to ensure IO::Async::OS->socket returns an IO::Socket::IP
+ instance for PF_INET or PF_INET6 if it is available
+ * Don't bother testing subsecond loop_once behaviour because most
+ loops can't actually do it
+ * Use Future::Utils instead of CPS, removing a dependency
+ * Removed IO::Async::Sequencer
+ * Print a deprecation warning on old loop classes with old timer
+ support
+
+0.54 CHANGES:
+ * Use Future instead of CPS::Future
+ * Created IO::Async::Future subclass
+ * Initial support for Futures on Loops
+ * Rewrite lots of internals to use Futures instead of MergePoints or
+ other logic
+ * Renamed all "task" to "future" in APIs
+ * Allow packing of inet/inet6 address structures to omit the IP or
+ port and presume passive or port 0
+ * Removed $notifier->get_loop synonym
+ * Make IO::Async::MergePoint throw a deprecation warning
+
+0.53 CHANGES:
+ * Added IO_ASYNC_WATCHDOG debugging support
+
+ BUGFIXES:
+ * Remember to return a task from Function->call even if it's queued
+ (RT79248)
+
+0.52 CHANGES:
+ * Initial attempt at Tasks using CPS::Future
+ * Minor fixes to timer LoopTests to prevent spurious failures of
+ sub-second timing
+ * Declare dependence on perl 5.10 now we're using 5.10 features
+ * Removed long-since deprecated IO::Async::DetachedCode
+
+0.51 CHANGES:
+ * Split much code out to new IO::Async::OS heirarchy
+ * Drop dead dependency on Test::Warn
+ * Smaller simpler signal handling, avoid POSIX::SigSet
+ * Expose getfamilybyname and getsocktypebyname as OS methods
+
+ BUGFIXES:
+ * Many small MSWin32 fixes that might help pass some tests. Likely
+ still incomplete though
+
+0.50 CHANGES:
+ * Added IO::Async::File
+ * Added filename mode of IO::Async::FileStream
+ * Make Heap dependency optional by reimplementing a tiny but less
+ efficient version of TimeQueue using a plain array
+ * No longer need MB-only Build.PL
+
+ BUGFIXES:
+ * Round up select() and poll() timeouts to nearest milisecond, might
+ help correct wait-time vs. gettimeofday() mismatches
+ * Fake read- and write-readiness of S_ISREG filehandles in select()
+ on MSWin32
+ * select() for exceptional status on MSWin32 to check for connect()
+ failures
+ * Don't unit-test that getsockname() works on socketpair()ed sockets
+
+0.49 CHANGES:
+ * Fix watch_time => enqueue_timer conversions; fix unit tests and
+ Timer implementations
+
+0.48 CHANGES:
+ * Support Channel long-running on_recv handler
+ * Support Channel directly between two Routines, in sync. mode at
+ both ends
+ * Added Loop->{watch,unwatch}_time API
+ * Added Function->restart and max_worker_calls parameter
+ * Support other reschedule policies for Timer::Periodic to allow
+ tick skipping, or drift
+
+ BUGFIXES:
+ * Fix example in Routine SYNOPSIS (thanks apeiron)
+ * Connector should check definedness of local_{host,port} rather than
+ existence
+
+0.47 CHANGES:
+ * Support $listener->listen( v6only => ... )
+ * Added new data-passing $loop->run and $loop->stop methods
+
+ BUGFIXES:
+ * Emulate ->socketpair on MSWin32 by connecting to a temporary socket
+ * Account for EWOULDBLOCK on MSWin32
+ * Don't try reading STDIN to block awaiting a signal in unit-tests
+ * Allow zero-delay Countdown timers (RT75954)
+ * Handle dup2() collisions in ChildManager filehandle setups
+ (RT75573)
+ * Fix race condition in t/33process.t (RT75573)
+ * Ensure Timer->stop doesn't fail if the timer isn't running
+ (RT75571)
+ * Possibly-fix some cygwin test failures (RT71706)
+ * Ensure that 'passive' getaddrinfo hint is handled in both
+ synchronous and numeric cases
+
+0.46 ADDITIONS:
+ * IO::Async::Routine + IO::Async::Channel
+ * IO::Async::Process->kill method
+
+ CHANGES:
+ * Use Socket 1.93 rather than dual-dependency logic on
+ Socket::GetAddrInfo
+ * Rewrote ::Function based on ::Routine and ::Channel
+ * Cleaner refcount behaviour in ::Process
+ * ::Process no longer waits for EOF condition on write-only pipes
+ * Don't unit-test the reading end of a pipe for HUP condition
+ * Documentation updates
+ * Removed documentation for long-since deprecated $loop->detach_child
+ and ->detach_code methods
+
+0.45 CHANGES:
+ * Added Timer->is_expired predicate, remove_on_expire parameter
+ (RT71767)
+
+ BUGFIXES:
+ * Use fd3/4 in ::Function rather than STDIN/STDOUT, to avoid
+ corrupting the return channel if the body function prints (RT72448)
+ * Better error detection around setuid/setgid/setgroups (RT72407)
+ * IO::Handle->binmode is not available as a method before perl 5.12;
+ use CORE::binmode() instead
+ * Don't attempt to invoke a missing on_notifier callback in
+ Loop->listen (RT71768)
+
+0.44 CHANGES:
+ * Allow Process to have sockets as handles; including datagram
+ sockets
+
+ BUGFIXES:
+ * Extract TimeQueue entiries before firing them, in case they do
+ something weird like cancelling themselves (RT70231)
+ * Test dollarbang for EWOULDBLOCK which might help MSWin32
+ * Cope correctly with Function handles in the presence of -CS or
+ PERL_UNICODE=S
+
+0.43 CHANGES:
+ * Allow IO::Async::Notifier to be used as a non-principle mixin class
+ * Provide Notifier->loop accessor
+ * Added (still-experimental) Notifier debug features
+ * Deleted various deprecated features:
+ + Notifier to Handle upgrade
+ + Loop->enable_childmanager, Loop->disable_childmanager
+ * Print deprecation warnings on Loop->detach_code, Loop->detach_child
+ * Minor improvements to LoopTests
+
+0.42 BUGFIXES:
+ * Test Stream encoding errors on a sequence which still returns
+ U+FFFD immediately on 5.14.0 (RT69020)
+
+0.41 CHANGES:
+ * Support 'encoding' parameter in IO::Async::Stream
+ * Allow IO::Async::Stream->write with an empty string, for the
+ side-effect of setting an on_flush handler
+ * Support 'first_interval' parameter to IO::Async::Timer::Periodic
+ * Expanded documentation of timers
+
+ BUGFIXES:
+ * Explicitly 'use IO::Handle;'
+
+0.40 ADDITIONS:
+ * Added IO::Async::FileStream - RT66520
+ * Added IO::Async::Stream 'close_on_read_eof' parameter
+ * Added IO::Async::Listener 'on_accept_failure' event
+
+ CHANGES:
+ * Allow Loop->listen to be extended via extensions, similar to
+ ->connect
+ * Autoflush streams used in Function::Worker objects by default
+ * Default Resolver to idle_timeout=30, min_workers=0
+
+ BUGFIXES:
+ * Don't convert method names to CODErefs during _capture_weakself as
+ it breaks dynamic dispatch and code reload - RT65785
+ * Only calculate Timer::Periodic's next tick time if it actually has
+ a Loop
+ * Put primary GID first in a 'setgroups' list, otherwise some BSDs
+ get upset - RT65127
+ * Load getaddrinfo() from Socket or Socket::GetAddrInfo in
+ t/50resolver.t
+ * Remove the anonymous Listener from the Loop if Loop->listen fails
+ - RT66168
+ * Supply LocalPort => 0 to IO::Socket::INET constructor explicitly
+ during testing
+
+0.39 CHANGES:
+ * Added IO::Async::Notifier 'notifier_name' parameter, which may be
+ used in debugging code in a later version
+ * Added IO::Async::Stream on_write_eof event
+ * Complain about unrecognised keys in IO::Async::Loop->watch_io and
+ IO::Async::Stream->write
+
+ BUGFIXES:
+ * Don't claim on_hangup supported except on those places we know it
+ will be (Linux, FreeBSD >= 8.0)
+ * Fixed race condition in t/41detached-code.t
+ * Fixed race condition in IO::Async::Function
+
+0.38 ADDITIONS:
+ * IO::Async::Function
+ * IO::Async::Loop->notifiers accessor
+
+ CHANGES:
+ * Symbolic flags in IO::Async::Resolver as convenience for commonly
+ used flag constants
+ * Distribution now uses Test::Fatal rather than Test::Exception
+ * Resolver is now a subclass of Function, not DetachedCode
+
+ BUGFIXES:
+ * More robust detection of Socket vs Socket::GetAddrInfo
+ * Portability fix for ChildManager's FD_CLOEXEC flag
+
+0.37 ADDITIONS:
+ * Handle->close_read, ->close_write
+ * Stream on_read_eof event
+ * extract_addrinfo conveniences for 'inet', 'inet6' and 'unix'
+
+ CHANGES:
+ * Allow Process filehandles to set up plain pipes without read/write
+ behaviour on the associated Stream
+ * Renamed Loop->unpack_addrinfo to ->extract_addrinfo
+ * Prepare for Socket::getaddrinfo() in core; prefer it to
+ Socket::GetAddrInfo::getaddrinfo()
+
+0.36 ADDITIONS:
+ * IO::Async::Process
+
+ CHANGES:
+ * Allow prequeuing of ->write data in Stream
+ * Check that signal handling remains properly deferred in LoopTests
+ * Miscellaneous documentation and examples updates
+
+ BUGFIXES:
+ * RT 64558 - getaddrinfo() returns duplicate addresses for localhost
+ * Don't rely on having NI_NUMERICSERV
+
+0.35 ADDITIONS:
+ * Loop->unpack_addrinfo
+
+ CHANGES:
+ * Recognise 'inet' and 'unix' as socket families
+ * Recognise 'stream', 'dgram' and 'raw' as socket types
+ * Recognise nicer HASH-based addrinfo layout in ->connect and
+ ->listen
+ * Listener now has on_stream / on_socket as full events, not just
+ CODEref parameters
+ * Make Resolver->getaddrinfo try synchronously if given numeric names
+ * Make Resolver->getnameinfo run synchronously if given
+ NI_NUMERICHOST|NI_NUMERICSERV flags
+ * Try to combine small data buffers from Stream->write calls if
+ possible
+
+ BUGFIXES:
+ * Linefeed in die case of getaddrinfo_hash to preserve exeception
+ string
+ * Deconfigure Protocol->transport after it is closed
+
+0.34 ADDITIONS:
+ * New Notifier methods ->_replace_weakself, ->maybe_invoke_event,
+ ->maybe_make_event_cb
+ * New Protocol method ->connect
+ * New subclass Protocol::LineStream
+ * Direct Resolver->getaddrinfo and ->getnameinfo methods
+
+ CHANGES:
+ * New Protocol::Stream->new( handle => $io ) parameters, which
+ creates an IO::Async::Stream to use as a transport
+ * Renamed Loop->detach_child to Loop->fork
+ * Pass errno values into ->connect on_connect_error and
+ ->listen on_listen_error
+ * Support timeouts on Resolver operations
+ * Allow direct access to Resolver via Loop->resolver
+
+ BUGFIXES:
+ * Make sure Protocol::Stream handles writersub and on_flush callback
+
+0.33 ADDITIONS:
+ * Allow watching child PID 0, to capture every child process exit
+ * $loop->time accessor
+ * Stream->write( sub { ... } ) dynamic stream generation
+ * Stream->write( $data, on_flush => sub { ... } ) callback
+
+ CHANGES:
+ * IO::Async::Loop->new magic constructor now caches the loop; useful
+ for wrapping modules, other event system integration, etc..
+
+0.32 ADDITIONS:
+ * IO::Async::Timer::Absolute
+ * Listener accessors for ->sockname, ->family, ->socktype
+
+ CHANGES:
+ * Implement and document Handle's want_{read,write}ready parameters
+ * Rearranged documentation for Notifier subclasses; new EVENTS
+ sections
+ * Correct location for #io-async channel on irc.perl.org
+
+0.31 ADDITIONS:
+ * Delegate Protocol->close method and on_closed continuation to its
+ transport object
+ * Stream->new_for_stdin, ->new_for_stdout, ->new_for_stdio
+ * Support Listener->new( handle => $fh )
+ * IO::Async::PID notifier subclass
+
+ CHANGES:
+ * Better documentation of Listener and Connector addr and addrs
+ arguments
+
+ BUGFIXES:
+ * INADDR_ANY/INADDR_LOOPBACK fixes inside BSD jails with restricted
+ networking
+
+0.30 ADDITIONS:
+ * Added IO::Async::Socket
+ * Added IO::Async::Protocol and ::Protocol::Stream
+ * Added on_stream and on_socket continuations for $loop->connect and
+ Listener
+
+ CHANGES:
+ * Emulate socketpair(AF_INET,...)
+ * Allow IO::Async::Stream 's read_len and write_len to be configured
+ per-instance
+ * Allow a Stream object without an on_read handler
+
+ BUGFIXES:
+ * Cope with exceptional-state sockets in Loop::Poll
+
+0.29 CHANGES:
+ * Don't require 'CODE' refs for callbacks/continations; this allows
+ the use of CODEref objects, &{} operator overloads, or other things
+ that are callable
+ * Implement 'read_all' and 'write_all' options on IO::Async::Stream
+ * Allow IO::Async::Stream subclasses to override on_closed
+
+ BUGFIXES:
+ * Work around some OSes not implementing SO_ACCEPTCONN
+ * Ensure Handle's on_read_ready/on_write_ready callbacks also take a
+ $self reference
+
+0.28 BUGFIXES:
+ * Ensure that Timer->start returns $self even when not in a Loop
+ * Accept bare GLOB refs as IO::Async::Listener handles; upgrade them
+ to IO::Socket refs if required
+ * Applied documentation patch from RT 55375 - thanks to
+ Chris Williams
+
+0.27 CHANGES:
+ * Implement 'autoflush' option on IO::Async::Stream
+
+ BUGFIXES:
+ * Avoid $_ breaking stored signal handler references when invoking
+ them
+ * Ignore EINTR from sysread/syswrite
+ * More reliable socket address tests - don't rely on uninitialised
+ padding bytes between struct members
+
+0.26 BUGFIXES:
+ * Connect to INADDR_LOOPBACK rather than INADDR_ANY during
+ t/24listener.t; hopefully fixes FAILs on OpenBSD
+ * Fix IO::Async::Stream during combined read/write-ready of a closed
+ stream
+
+0.25 CHANGES:
+ * Accept 'stream'/'dgram'/'raw' as symbolic shortcuts for socket
+ types in connect/listen operations - avoids 'use Socket'
+ * Accept IO::Handle-derived objects in ChildManager setup keys as
+ well as raw GLOB refs
+
+ BUGFIXES:
+ * Various changes to test scripts to hopefully improve portability or
+ reliability during smoke tests
+
+0.24 ADDITIONS:
+ * Timer subclasses - Countdown and Periodic
+ * Idleness event watching via low-level 'watch_io/unwatch_io' methods
+ and higher-level 'later' method
+ * Added the missing 'unwatch_child' method
+ * Shareable acceptance testing suite for IO::Async::Loop subclasses
+ for better testing in subclass implementations
+
+ CHANGES:
+ * More future-proof API version checking for subclasses - requires
+ subclasses to declare their version.
+ ### pre-0.24 Loop subclasses are no longer compatible. ###
+ * Entirely remove the need to $loop->enable_childmanager by calling
+ waitpid() in 'watch_child'.
+
+0.23 CHANGES:
+ * Rearranged IO::Async::Listener to be a constructable Notifier
+ suclass
+ * Allow Signal, Timer and Listener to act as base classes as well as
+ standalone with callbacks
+ * Renamed IO::Async::Loop::IO_Poll to ::Poll; created transparent
+ backward-compatibility wrapper
+
+0.22 CHANGES:
+ * Added tcp-proxy.pl example
+ * More documentation on IO::Async::Notifier subclass-override methods
+ * Documented that IO::Async::MergePoint is just an Async::MergePoint
+ * Various small updates to keep CPANTS happy
+
+ BUGFIXES:
+ * Don't test Async::MergePoint locally as it's now a separate dist,
+ and the tests here were reporting false negatives.
+
+0.21 CHANGES:
+ * Added "use warnings" to all modules
+ * Created Notifier->configure method to allow changing properties of
+ a Notifier or subclass after construction
+ * New 'examples' dir with some small example scripts
+
+ BUGFIXES:
+ * More robust timing tests to avoid some spurious test failures due
+ to busy testing servers or other non-issues
+
+0.20 CHANGES:
+ * Major reworking of underlying Loop implementation:
+ + Unified low-level IO, timer and signal watches as callbacks
+ + Split IO handle parts of Notifier into new IO::Async::Handle
+ class
+ + Created Timer and Signal subclasses of Notifier
+
+ These changes will require a compatible upgrade to the underlying
+ Loop implementation.
+
+ * Hide SignalProxy and TimeQueue from CPAN's indexer, as they are
+ internal-only details that don't need exposing there.
+ * Loop magic constructor now warns if a specifically-requested class
+ is not available
+ * Allow multiple attachment of signals via Loop->attach_signal or new
+ Signal objects
+
+0.19 CHANGES:
+ * Allow control of Sequencer's pipelining
+ * Documentation fixes
+ * Allow Loop->run_child to take a 'setup' array
+ * Added 'setuid', 'setgid' and 'setgroups' child setup operations
+ * Support 'on_notifier' in Loop->listen
+
+ BUGFIXES:
+ * carp before return in Stream->write so it actually prints
+ * Ensure Streams still work after being closed and reopened by
+ ->set_handle
+ * If IO::Socket->new() fails, try again with generic ->socket
+ (makes IPv6 work on platforms without IO::Socket::INET6)
+
+0.18 CHANGES:
+ * Allow Sequencer to be a base class as well as using constructor
+ callbacks
+ * Use signal names from Config.pm rather than relying on POSIX.pm.
+ Covers more signals that way
+
+ BUGFIXES:
+ * Gracefully handle accept() returning EAGAIN
+ * Fixed handling of IO::Socket->getsockopt( SOL_SOCKET, SO_ERROR )
+
+0.17 CHANGES:
+ * Added Stream->close_when_empty and ->close_now. Added docs
+ * Added OS abstractions of socketpair() and pipe()
+ * Many documentation changes and updates
+
+ BUGFIXES:
+ * Properly handle stream read/write errors; close immediately rather
+ than deferring until empty.
+ * Various CPAN testers somketest bug fixes
+ * Fixed http://rt.cpan.org/Ticket/Display.html?id=38476
+
+0.16 ADDITIONS:
+ * Loop->requeue_timer()
+ * Magic constructor in IO::Async::Loop which tries to find the best
+ subclass
+ * 'chdir' and 'nice' ChildManager operations
+
+ CHANGES:
+ * Make sure that top-level objects are refcount-clean by using
+ Test::Refcount, and Scalar::Util::weaken()
+
+ BUGFIXES:
+ * Keep perl 5.6.1 happy by not passing LocalPort => 0 when
+ constructing IO::Socket::INETs
+ * Pass the Type option to IO::Socket::INET constructor in test
+ scripts
+
+0.15 REMOVALS:
+ * IO::Async::Set subclasses and IO::Async::Buffer have now been
+ entirely removed.
+
+ CHANGES:
+ * Support handle-less IO::Async::Sequencer, like ::Notifier
+ * Set SO_REUSEADDR on listening sockets by default
+ * Allow Loop->listen() on a plain filehandle containing a socket
+ * No longer any need to explcitly call Loop->enable_childmanager
+
+ BUGFIXES:
+ * IO::Async::Loop->_adjust_timeout actually works properly
+ * Notifier->close() only runs on_closed callback if it actually
+ closed - allows for neater cross-connected Notifiers
+ * Made Notifier->want_{read,write}ready more efficient
+ * Notifier->close() on a child notifier works
+ * Loop->listen() should take the first successful address, rather
+ than trying them all
+
+0.14 REMOVALS:
+ * IO::Async::Set subclasses and IO::Async::Buffer are now completely
+ deprecated. Any attempt to use them will fail immediately.
+
+ ADDITIONS:
+ * 'keep' ChildManager operation
+ * IO::Async::Test::wait_for_stream()
+ * Loop->listen()
+ * IO::Async::Sequencer class
+
+ CHANGES:
+ * Support dynamic swapping of temporary 'on_read' handlers in Stream
+ * Now requires Socket::GetAddrInfo >= 0.08
+ * Further shortcuts in ChildManager setup keys - IO references and
+ simple string operation names
+ * Support handle-less IO::Async::Notifiers that have IO handles added
+ to them later
+ * Allow 'setup' key to Loop->detach_code()
+ * Various documentation updates
+
+ BUGFIXES:
+ * Allow the same filehandle to be 'dup'ed more than once in
+ ChildManager
+
+0.13 CHANGES:
+ * Flush all awaiting data from Stream when it becomes writeready
+ * Supply a real IO::Async::Test module to allow testing in 3rd party
+ distros
+ * Various documentation fixes
+
+ BUGFIXES:
+ * Don't rely on STDOUT being writable during test scripts
+
+0.12 CHANGES:
+ * Allow Notifiers that are write-only.
+ * Added ChildManager->open and ->run; with ->open_child and
+ ->run_child on the containing Loop.
+ * Moved IO::Async::Loop::Glib out to its own CPAN dist, to
+ simplify Build.PL and testing scripts
+
+ BUGFIXES:
+ * Make sure to "use IO::Socket" in IO::Async::Connector
+ * Pass 'socktype' argument to ->connect during testing
+
+0.11 INCOMPATIBLE CHANGES:
+ * Renamed IO::Async::Set::* to IO::Async::Loop::* - provided
+ backward-compatibility wrappers around old names.
+ IO::Async::Set::GMainLoop has become IO::Async::Lib::Glib
+ * Renamed IO::Async::Buffer to IO::Async::Stream - provided backward-
+ compatibility wrapper around old name.
+ * Loop->get_childmanager() and ->get_sigproxy() no longer allowed
+
+ CHANGES:
+ * Extended ->loop_once() and ->loop() feature out to all
+ IO::Async::Loop classes
+ * Added IO::Async::Resolver and IO::Async::Connector, plus Loop
+ integration
+ * Allow write-only IO::Async::Notifiers that have no read handle or
+ readiness callback.
+
+0.10 INCOMPATIBLE CHANGES:
+ * Renamed events and methods in IO::Async::Notifier to better fit the
+ naming scheme of normal Perl handles. Backward-compatibility hooks
+ are currently provided, but will be removed in a later release. Any
+ code using the old names should be updated
+
+ CHANGES:
+ * Allow DetachedCode to have multiple back-end worker processes.
+ * Control if a back-end worker exits when the code "die"s
+ * Added 'close()' method on Notifiers/Buffers. Sensible behaviour on
+ buffers with queued data to send
+ * Reset %SIG hash in ChildManager->detach_child()
+
+ BUGFIXES:
+ * Clean up temporary directory during testing
+ * Shut down DetachedCode workers properly on object deref
+ * Better handling of borderline timing failures in t/11set-*.t
+ * Close old handles before dup2()ing new ones when detaching code
+ * Various other minor test script improvements
+
+0.09 CHANGES:
+ * Added TimeQueue object and integration with IO::Async::Set and
+ subclasses.
+ * Added MergePoint object
+ * Added 'on_closed' callback support to IO::Async::Notifier
+
+ BUGFIXES:
+ * Don't depend on system locale when checking string value of $!
+ * Fixed test scripts to more closely approximate real code behaviour
+ in the presence of poll() vs. deferred signal delivery
+
+0.08 CHANGES:
+ * Added ChildManager->detach_child() method
+ * Added DetachedCode object
+
+ BUGFIXES:
+ * Better tests for presence of Glib to improve test false failures
+ * More lenient times in test script 11set-IO-Poll-timing to allow for
+ variances at test time
+ * Avoid bugs in post_select()/post_poll() caused by some notifier
+ callbacks removing other notifiers from the set
+
+0.07 BUGFIXES:
+ * Avoid race condition in t/30childmanager.t - wait for child process
+ to actually exit
+ * Avoid race condition in IO::Async::ChildManager->spawn() by waiting
+ for SIGCHLD+pipe close, rather than SIGCHLD+pipe data
+
+0.06 CHANGES:
+ * Allow 'env' setup key to ChildManager->spawn() to change the
+ child's %ENV
+ * Updated the way some of the ->spawn() tests are conducted. There
+ seems to be massive failures reported on cpantesters against 0.05.
+ These changes won't fix the bugs, but should assist in reporting
+ and tracking them down.
+
+ BUGFIXES:
+ * Don't rely on existence of /bin/true - test for /usr/bin/true as
+ well, fall back on "$^X -e 1"
+ * Avoid kernel race condition in t/32childmanager-spawn-setup.t by
+ proper use of select() when testing.
+
+0.05 CHANGES:
+ * Added ChildManager object
+ * Added singleton storage in IO::Async::Set to store a SignalProxy or
+ ChildManager conveniently
+
+ BUGFIXES:
+ * Workaround for a bug in IO::Poll version 0.05
+
+0.04 CHANGES:
+ * Added dynamic signal attach / detach methods to SignalProxy
+ * Buffer now has on_read_error / on_write_error callbacks for
+ handling IO errors on underlying sysread()/syswrite() calls
+
+0.03 CHANGES:
+ * No longer build_requires 'Glib' - print a warning if it's not
+ installed but carry on anyway.
+ * IO_Poll->loop_once() now returns the result from the poll() call
+ * Added concept of nested child notifiers within Notifier object
+
+ BUGFIXES:
+ * Fix to test scripts that call IO_Poll's loop_once() with a timeout
+ of zero. This can cause a kernel race condition, so supply some
+ small non-zero value instead.
+
+0.02 INCOMPATIBLE CHANGES:
+ * Event methods/callback functions now called "on_*" to distinguish
+ them
+ * Callback functions now pass $self as first argument to simplify
+ called code
+
+ CHANGES:
+ * Improved POD in Notifier.pm and Buffer.pm
+
+ BUGFIXES:
+ * GMainLoop.pm - return 1 from callbacks so that glib doesn't remove
+ our IO sources
+ * GMainLoop.pm - make sure re-asserting want_writeready actually adds
+ the IO source again
+
+0.01 First version, released on an unsuspecting world.
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..94306ca
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,379 @@
+This software is copyright (c) 2015 by Paul Evans <leonerd@leonerd.org.uk>.
+
+This is free software; you can redistribute it and/or modify it under
+the same terms as the Perl 5 programming language system itself.
+
+Terms of the Perl programming language system itself
+
+a) the GNU General Public License as published by the Free
+ Software Foundation; either version 1, or (at your option) any
+ later version, or
+b) the "Artistic License"
+
+--- The GNU General Public License, Version 1, February 1989 ---
+
+This software is Copyright (c) 2015 by Paul Evans <leonerd@leonerd.org.uk>.
+
+This is free software, licensed under:
+
+ The GNU General Public License, Version 1, February 1989
+
+ GNU GENERAL PUBLIC LICENSE
+ Version 1, February 1989
+
+ Copyright (C) 1989 Free Software Foundation, Inc.
+ 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The license agreements of most software companies try to keep users
+at the mercy of those companies. By contrast, our General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. The
+General Public License applies to the Free Software Foundation's
+software and to any other program whose authors commit to using it.
+You can use it for your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Specifically, the General Public License is designed to make
+sure that you have the freedom to give away or sell copies of free
+software, that you receive source code or can get it if you want it,
+that you can change the software or use pieces of it in new free
+programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of a such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must tell them their rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any program or other work which
+contains a notice placed by the copyright holder saying it may be
+distributed under the terms of this General Public License. The
+"Program", below, refers to any such program or work, and a "work based
+on the Program" means either the Program or any work containing the
+Program or a portion of it, either verbatim or with modifications. Each
+licensee is addressed as "you".
+
+ 1. You may copy and distribute verbatim copies of the Program's source
+code as you receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice and
+disclaimer of warranty; keep intact all the notices that refer to this
+General Public License and to the absence of any warranty; and give any
+other recipients of the Program a copy of this General Public License
+along with the Program. You may charge a fee for the physical act of
+transferring a copy.
+
+ 2. You may modify your copy or copies of the Program or any portion of
+it, and copy and distribute such modifications under the terms of Paragraph
+1 above, provided that you also do the following:
+
+ a) cause the modified files to carry prominent notices stating that
+ you changed the files and the date of any change; and
+
+ b) cause the whole of any work that you distribute or publish, that
+ in whole or in part contains the Program or any part thereof, either
+ with or without modifications, to be licensed at no charge to all
+ third parties under the terms of this General Public License (except
+ that you may choose to grant warranty protection to some or all
+ third parties, at your option).
+
+ c) If the modified program normally reads commands interactively when
+ run, you must cause it, when started running for such interactive use
+ in the simplest and most usual way, to print or display an
+ announcement including an appropriate copyright notice and a notice
+ that there is no warranty (or else, saying that you provide a
+ warranty) and that users may redistribute the program under these
+ conditions, and telling the user how to view a copy of this General
+ Public License.
+
+ d) You may charge a fee for the physical act of transferring a
+ copy, and you may at your option offer warranty protection in
+ exchange for a fee.
+
+Mere aggregation of another independent work with the Program (or its
+derivative) on a volume of a storage or distribution medium does not bring
+the other work under the scope of these terms.
+
+ 3. You may copy and distribute the Program (or a portion or derivative of
+it, under Paragraph 2) in object code or executable form under the terms of
+Paragraphs 1 and 2 above provided that you also do one of the following:
+
+ a) accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of
+ Paragraphs 1 and 2 above; or,
+
+ b) accompany it with a written offer, valid for at least three
+ years, to give any third party free (except for a nominal charge
+ for the cost of distribution) a complete machine-readable copy of the
+ corresponding source code, to be distributed under the terms of
+ Paragraphs 1 and 2 above; or,
+
+ c) accompany it with the information you received as to where the
+ corresponding source code may be obtained. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form alone.)
+
+Source code for a work means the preferred form of the work for making
+modifications to it. For an executable file, complete source code means
+all the source code for all modules it contains; but, as a special
+exception, it need not include source code for modules which are standard
+libraries that accompany the operating system on which the executable
+file runs, or for standard header files or definitions files that
+accompany that operating system.
+
+ 4. You may not copy, modify, sublicense, distribute or transfer the
+Program except as expressly provided under this General Public License.
+Any attempt otherwise to copy, modify, sublicense, distribute or transfer
+the Program is void, and will automatically terminate your rights to use
+the Program under this License. However, parties who have received
+copies, or rights to use copies, from you under this General Public
+License will not have their licenses terminated so long as such parties
+remain in full compliance.
+
+ 5. By copying, distributing or modifying the Program (or any work based
+on the Program) you indicate your acceptance of this license to do so,
+and all its terms and conditions.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the original
+licensor to copy, distribute or modify the Program subject to these
+terms and conditions. You may not impose any further restrictions on the
+recipients' exercise of the rights granted herein.
+
+ 7. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of the license which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+the license, you may choose any version ever published by the Free Software
+Foundation.
+
+ 8. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 9. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 10. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ Appendix: How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to humanity, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these
+terms.
+
+ To do so, attach the following notices to the program. It is safest to
+attach them to the start of each source file to most effectively convey
+the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) 19yy <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 1, or (at your option)
+ any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301 USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) 19xx name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the
+appropriate parts of the General Public License. Of course, the
+commands you use may be called something other than `show w' and `show
+c'; they could even be mouse-clicks or menu items--whatever suits your
+program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ program `Gnomovision' (a program to direct compilers to make passes
+ at assemblers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+That's all there is to it!
+
+
+--- The Artistic License 1.0 ---
+
+This software is Copyright (c) 2015 by Paul Evans <leonerd@leonerd.org.uk>.
+
+This is free software, licensed under:
+
+ The Artistic License 1.0
+
+The Artistic License
+
+Preamble
+
+The intent of this document is to state the conditions under which a Package
+may be copied, such that the Copyright Holder maintains some semblance of
+artistic control over the development of the package, while giving the users of
+the package the right to use and distribute the Package in a more-or-less
+customary fashion, plus the right to make reasonable modifications.
+
+Definitions:
+
+ - "Package" refers to the collection of files distributed by the Copyright
+ Holder, and derivatives of that collection of files created through
+ textual modification.
+ - "Standard Version" refers to such a Package if it has not been modified,
+ or has been modified in accordance with the wishes of the Copyright
+ Holder.
+ - "Copyright Holder" is whoever is named in the copyright or copyrights for
+ the package.
+ - "You" is you, if you're thinking about copying or distributing this Package.
+ - "Reasonable copying fee" is whatever you can justify on the basis of media
+ cost, duplication charges, time of people involved, and so on. (You will
+ not be required to justify it to the Copyright Holder, but only to the
+ computing community at large as a market that must bear the fee.)
+ - "Freely Available" means that no fee is charged for the item itself, though
+ there may be fees involved in handling the item. It also means that
+ recipients of the item may redistribute it under the same conditions they
+ received it.
+
+1. You may make and give away verbatim copies of the source form of the
+Standard Version of this Package without restriction, provided that you
+duplicate all of the original copyright notices and associated disclaimers.
+
+2. You may apply bug fixes, portability fixes and other modifications derived
+from the Public Domain or from the Copyright Holder. A Package modified in such
+a way shall still be considered the Standard Version.
+
+3. You may otherwise modify your copy of this Package in any way, provided that
+you insert a prominent notice in each changed file stating how and when you
+changed that file, and provided that you do at least ONE of the following:
+
+ a) place your modifications in the Public Domain or otherwise make them
+ Freely Available, such as by posting said modifications to Usenet or an
+ equivalent medium, or placing the modifications on a major archive site
+ such as ftp.uu.net, or by allowing the Copyright Holder to include your
+ modifications in the Standard Version of the Package.
+
+ b) use the modified Package only within your corporation or organization.
+
+ c) rename any non-standard executables so the names do not conflict with
+ standard executables, which must also be provided, and provide a separate
+ manual page for each non-standard executable that clearly documents how it
+ differs from the Standard Version.
+
+ d) make other distribution arrangements with the Copyright Holder.
+
+4. You may distribute the programs of this Package in object code or executable
+form, provided that you do at least ONE of the following:
+
+ a) distribute a Standard Version of the executables and library files,
+ together with instructions (in the manual page or equivalent) on where to
+ get the Standard Version.
+
+ b) accompany the distribution with the machine-readable source of the Package
+ with your modifications.
+
+ c) accompany any non-standard executables with their corresponding Standard
+ Version executables, giving the non-standard executables non-standard
+ names, and clearly documenting the differences in manual pages (or
+ equivalent), together with instructions on where to get the Standard
+ Version.
+
+ d) make other distribution arrangements with the Copyright Holder.
+
+5. You may charge a reasonable copying fee for any distribution of this
+Package. You may charge any fee you choose for support of this Package. You
+may not charge a fee for this Package itself. However, you may distribute this
+Package in aggregate with other (possibly commercial) programs as part of a
+larger (possibly commercial) software distribution provided that you do not
+advertise this Package as a product of your own.
+
+6. The scripts and library files supplied as input to or produced as output
+from the programs of this Package do not automatically fall under the copyright
+of this Package, but belong to whomever generated them, and may be sold
+commercially, and may be aggregated with this Package.
+
+7. C or perl subroutines supplied by you and linked into this Package shall not
+be considered part of this Package.
+
+8. The name of the Copyright Holder may not be used to endorse or promote
+products derived from this software without specific prior written permission.
+
+9. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
+WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
+MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
+
+The End
+
diff --git a/MANIFEST b/MANIFEST
new file mode 100644
index 0000000..6c6c035
--- /dev/null
+++ b/MANIFEST
@@ -0,0 +1,113 @@
+Build.PL
+Changes
+examples/chat-server.pl
+examples/echo-server.pl
+examples/netcat-client.pl
+examples/readwrite-futures.pl
+examples/tail-logfile.pl
+examples/tcp-proxy.pl
+examples/whoami-server.pl
+lib/IO/Async.pm
+lib/IO/Async/Channel.pm
+lib/IO/Async/ChildManager.pm
+lib/IO/Async/Debug.pm
+lib/IO/Async/File.pm
+lib/IO/Async/FileStream.pm
+lib/IO/Async/Function.pm
+lib/IO/Async/Future.pm
+lib/IO/Async/Handle.pm
+lib/IO/Async/Internals/Connector.pm
+lib/IO/Async/Internals/TimeQueue.pm
+lib/IO/Async/Listener.pm
+lib/IO/Async/Loop.pm
+lib/IO/Async/Loop/Poll.pm
+lib/IO/Async/Loop/Select.pm
+lib/IO/Async/LoopTests.pm
+lib/IO/Async/Notifier.pm
+lib/IO/Async/OS.pm
+lib/IO/Async/OS/cygwin.pm
+lib/IO/Async/OS/linux.pm
+lib/IO/Async/OS/MSWin32.pm
+lib/IO/Async/PID.pm
+lib/IO/Async/Process.pm
+lib/IO/Async/Protocol.pm
+lib/IO/Async/Protocol/LineStream.pm
+lib/IO/Async/Protocol/Stream.pm
+lib/IO/Async/Resolver.pm
+lib/IO/Async/Routine.pm
+lib/IO/Async/Signal.pm
+lib/IO/Async/Socket.pm
+lib/IO/Async/Stream.pm
+lib/IO/Async/Test.pm
+lib/IO/Async/Timer.pm
+lib/IO/Async/Timer/Absolute.pm
+lib/IO/Async/Timer/Countdown.pm
+lib/IO/Async/Timer/Periodic.pm
+LICENSE
+Makefile.PL
+MANIFEST This list of files
+META.json
+META.yml
+README
+t/00use.t
+t/01timequeue.t
+t/02os.t
+t/03loop-magic.t
+t/04notifier.t
+t/05notifier-loop.t
+t/06notifier-mixin.t
+t/07notifier-future.t
+t/10loop-poll-io.t
+t/10loop-select-io.t
+t/11loop-poll-timer.t
+t/11loop-select-timer.t
+t/12loop-poll-signal.t
+t/12loop-select-signal.t
+t/13loop-poll-idle.t
+t/13loop-select-idle.t
+t/14loop-poll-child.t
+t/14loop-select-child.t
+t/15loop-poll-control.t
+t/15loop-select-control.t
+t/18loop-poll-legacy.t
+t/18loop-select-legacy.t
+t/19loop-future.t
+t/19test.t
+t/20handle.t
+t/21stream-1read.t
+t/21stream-2write.t
+t/21stream-3split.t
+t/21stream-4encoding.t
+t/22timer-absolute.t
+t/22timer-countdown.t
+t/22timer-periodic.t
+t/23signal.t
+t/24listener.t
+t/25socket.t
+t/26pid.t
+t/27file.t
+t/28filestream.t
+t/30loop-fork.t
+t/31loop-spawnchild.t
+t/32loop-spawnchild-setup.t
+t/33process.t
+t/34process-handles.t
+t/35loop-openchild.t
+t/36loop-runchild.t
+t/37loop-child-root.t
+t/38loop-thread.t
+t/40channel.t
+t/41routine.t
+t/42function.t
+t/50resolver.t
+t/51loop-connect.t
+t/52loop-listen.t
+t/53loop-extend.t
+t/60protocol.t
+t/61protocol-stream.t
+t/62protocol-linestream.t
+t/63handle-connect.t
+t/64handle-bind.t
+t/99pod.t
+t/StupidLoop.pm
+t/TimeAbout.pm
diff --git a/META.json b/META.json
new file mode 100644
index 0000000..77300d3
--- /dev/null
+++ b/META.json
@@ -0,0 +1,195 @@
+{
+ "abstract" : "Asynchronous event-driven programming",
+ "author" : [
+ "Paul Evans <leonerd@leonerd.org.uk>"
+ ],
+ "dynamic_config" : 1,
+ "generated_by" : "Module::Build version 0.421",
+ "license" : [
+ "perl_5"
+ ],
+ "meta-spec" : {
+ "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec",
+ "version" : "2"
+ },
+ "name" : "IO-Async",
+ "prereqs" : {
+ "configure" : {
+ "requires" : {
+ "Module::Build" : "0.4004"
+ }
+ },
+ "runtime" : {
+ "recommends" : {
+ "IO::Socket::IP" : "0"
+ },
+ "requires" : {
+ "Exporter" : "5.57",
+ "File::stat" : "0",
+ "Future" : "0.26",
+ "Future::Utils" : "0.18",
+ "IO::Poll" : "0",
+ "Socket" : "2.007",
+ "Storable" : "0",
+ "Struct::Dumb" : "0",
+ "Time::HiRes" : "0",
+ "perl" : "5.010"
+ }
+ },
+ "test" : {
+ "requires" : {
+ "File::Temp" : "0",
+ "Test::Fatal" : "0",
+ "Test::Identity" : "0",
+ "Test::More" : "0.88",
+ "Test::Refcount" : "0"
+ }
+ }
+ },
+ "provides" : {
+ "IO::Async" : {
+ "file" : "lib/IO/Async.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::Channel" : {
+ "file" : "lib/IO/Async/Channel.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::ChildManager" : {
+ "file" : "lib/IO/Async/ChildManager.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::Debug" : {
+ "file" : "lib/IO/Async/Debug.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::File" : {
+ "file" : "lib/IO/Async/File.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::FileStream" : {
+ "file" : "lib/IO/Async/FileStream.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::Function" : {
+ "file" : "lib/IO/Async/Function.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::Future" : {
+ "file" : "lib/IO/Async/Future.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::Handle" : {
+ "file" : "lib/IO/Async/Handle.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::Listener" : {
+ "file" : "lib/IO/Async/Listener.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::Loop" : {
+ "file" : "lib/IO/Async/Loop.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::Loop::Poll" : {
+ "file" : "lib/IO/Async/Loop/Poll.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::Loop::Select" : {
+ "file" : "lib/IO/Async/Loop/Select.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::LoopTests" : {
+ "file" : "lib/IO/Async/LoopTests.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::Notifier" : {
+ "file" : "lib/IO/Async/Notifier.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::OS" : {
+ "file" : "lib/IO/Async/OS.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::OS::MSWin32" : {
+ "file" : "lib/IO/Async/OS/MSWin32.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::OS::cygwin" : {
+ "file" : "lib/IO/Async/OS/cygwin.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::OS::linux" : {
+ "file" : "lib/IO/Async/OS/linux.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::PID" : {
+ "file" : "lib/IO/Async/PID.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::Process" : {
+ "file" : "lib/IO/Async/Process.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::Protocol" : {
+ "file" : "lib/IO/Async/Protocol.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::Protocol::LineStream" : {
+ "file" : "lib/IO/Async/Protocol/LineStream.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::Protocol::Stream" : {
+ "file" : "lib/IO/Async/Protocol/Stream.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::Resolver" : {
+ "file" : "lib/IO/Async/Resolver.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::Routine" : {
+ "file" : "lib/IO/Async/Routine.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::Signal" : {
+ "file" : "lib/IO/Async/Signal.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::Socket" : {
+ "file" : "lib/IO/Async/Socket.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::Stream" : {
+ "file" : "lib/IO/Async/Stream.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::Test" : {
+ "file" : "lib/IO/Async/Test.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::Timer" : {
+ "file" : "lib/IO/Async/Timer.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::Timer::Absolute" : {
+ "file" : "lib/IO/Async/Timer/Absolute.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::Timer::Countdown" : {
+ "file" : "lib/IO/Async/Timer/Countdown.pm",
+ "version" : "0.67"
+ },
+ "IO::Async::Timer::Periodic" : {
+ "file" : "lib/IO/Async/Timer/Periodic.pm",
+ "version" : "0.67"
+ }
+ },
+ "release_status" : "stable",
+ "resources" : {
+ "license" : [
+ "http://dev.perl.org/licenses/"
+ ],
+ "x_IRC" : "irc://irc.perl.org/#io-async"
+ },
+ "version" : "0.67"
+}
diff --git a/META.yml b/META.yml
new file mode 100644
index 0000000..26967a1
--- /dev/null
+++ b/META.yml
@@ -0,0 +1,139 @@
+---
+abstract: 'Asynchronous event-driven programming'
+author:
+ - 'Paul Evans <leonerd@leonerd.org.uk>'
+build_requires:
+ File::Temp: '0'
+ Test::Fatal: '0'
+ Test::Identity: '0'
+ Test::More: '0.88'
+ Test::Refcount: '0'
+configure_requires:
+ Module::Build: '0.4004'
+dynamic_config: 1
+generated_by: 'Module::Build version 0.421, CPAN::Meta::Converter version 2.142690'
+license: perl
+meta-spec:
+ url: http://module-build.sourceforge.net/META-spec-v1.4.html
+ version: '1.4'
+name: IO-Async
+provides:
+ IO::Async:
+ file: lib/IO/Async.pm
+ version: '0.67'
+ IO::Async::Channel:
+ file: lib/IO/Async/Channel.pm
+ version: '0.67'
+ IO::Async::ChildManager:
+ file: lib/IO/Async/ChildManager.pm
+ version: '0.67'
+ IO::Async::Debug:
+ file: lib/IO/Async/Debug.pm
+ version: '0.67'
+ IO::Async::File:
+ file: lib/IO/Async/File.pm
+ version: '0.67'
+ IO::Async::FileStream:
+ file: lib/IO/Async/FileStream.pm
+ version: '0.67'
+ IO::Async::Function:
+ file: lib/IO/Async/Function.pm
+ version: '0.67'
+ IO::Async::Future:
+ file: lib/IO/Async/Future.pm
+ version: '0.67'
+ IO::Async::Handle:
+ file: lib/IO/Async/Handle.pm
+ version: '0.67'
+ IO::Async::Listener:
+ file: lib/IO/Async/Listener.pm
+ version: '0.67'
+ IO::Async::Loop:
+ file: lib/IO/Async/Loop.pm
+ version: '0.67'
+ IO::Async::Loop::Poll:
+ file: lib/IO/Async/Loop/Poll.pm
+ version: '0.67'
+ IO::Async::Loop::Select:
+ file: lib/IO/Async/Loop/Select.pm
+ version: '0.67'
+ IO::Async::LoopTests:
+ file: lib/IO/Async/LoopTests.pm
+ version: '0.67'
+ IO::Async::Notifier:
+ file: lib/IO/Async/Notifier.pm
+ version: '0.67'
+ IO::Async::OS:
+ file: lib/IO/Async/OS.pm
+ version: '0.67'
+ IO::Async::OS::MSWin32:
+ file: lib/IO/Async/OS/MSWin32.pm
+ version: '0.67'
+ IO::Async::OS::cygwin:
+ file: lib/IO/Async/OS/cygwin.pm
+ version: '0.67'
+ IO::Async::OS::linux:
+ file: lib/IO/Async/OS/linux.pm
+ version: '0.67'
+ IO::Async::PID:
+ file: lib/IO/Async/PID.pm
+ version: '0.67'
+ IO::Async::Process:
+ file: lib/IO/Async/Process.pm
+ version: '0.67'
+ IO::Async::Protocol:
+ file: lib/IO/Async/Protocol.pm
+ version: '0.67'
+ IO::Async::Protocol::LineStream:
+ file: lib/IO/Async/Protocol/LineStream.pm
+ version: '0.67'
+ IO::Async::Protocol::Stream:
+ file: lib/IO/Async/Protocol/Stream.pm
+ version: '0.67'
+ IO::Async::Resolver:
+ file: lib/IO/Async/Resolver.pm
+ version: '0.67'
+ IO::Async::Routine:
+ file: lib/IO/Async/Routine.pm
+ version: '0.67'
+ IO::Async::Signal:
+ file: lib/IO/Async/Signal.pm
+ version: '0.67'
+ IO::Async::Socket:
+ file: lib/IO/Async/Socket.pm
+ version: '0.67'
+ IO::Async::Stream:
+ file: lib/IO/Async/Stream.pm
+ version: '0.67'
+ IO::Async::Test:
+ file: lib/IO/Async/Test.pm
+ version: '0.67'
+ IO::Async::Timer:
+ file: lib/IO/Async/Timer.pm
+ version: '0.67'
+ IO::Async::Timer::Absolute:
+ file: lib/IO/Async/Timer/Absolute.pm
+ version: '0.67'
+ IO::Async::Timer::Countdown:
+ file: lib/IO/Async/Timer/Countdown.pm
+ version: '0.67'
+ IO::Async::Timer::Periodic:
+ file: lib/IO/Async/Timer/Periodic.pm
+ version: '0.67'
+recommends:
+ IO::Socket::IP: '0'
+requires:
+ Exporter: '5.57'
+ File::stat: '0'
+ Future: '0.26'
+ Future::Utils: '0.18'
+ IO::Poll: '0'
+ Socket: '2.007'
+ Storable: '0'
+ Struct::Dumb: '0'
+ Time::HiRes: '0'
+ perl: '5.010'
+resources:
+ IRC: irc://irc.perl.org/#io-async
+ license: http://dev.perl.org/licenses/
+version: '0.67'
diff --git a/Makefile.PL b/Makefile.PL
new file mode 100644
index 0000000..8275873
--- /dev/null
+++ b/Makefile.PL
@@ -0,0 +1,23 @@
+# Note: this file was auto-generated by Module::Build::Compat version 0.4210
+require 5.010;
+use ExtUtils::MakeMaker;
+WriteMakefile
+(
+ 'NAME' => 'IO::Async',
+ 'VERSION_FROM' => 'lib/IO/Async.pm',
+ 'PREREQ_PM' => {
+ 'Exporter' => '5.57',
+ 'File::stat' => 0,
+ 'Future' => '0.26',
+ 'Future::Utils' => '0.18',
+ 'IO::Poll' => 0,
+ 'Socket' => '2.007',
+ 'Storable' => 0,
+ 'Struct::Dumb' => 0,
+ 'Time::HiRes' => 0
+ },
+ 'INSTALLDIRS' => 'site',
+ 'EXE_FILES' => [],
+ 'PL_FILES' => {}
+)
+;
diff --git a/README b/README
new file mode 100644
index 0000000..49cd75d
--- /dev/null
+++ b/README
@@ -0,0 +1,278 @@
+NAME
+ `IO::Async' - Asynchronous event-driven programming
+
+SYNOPSIS
+ use IO::Async::Stream;
+ use IO::Async::Loop;
+
+ my $loop = IO::Async::Loop->new;
+
+ $loop->connect(
+ host => "some.other.host",
+ service => 12345,
+ socktype => 'stream',
+
+ on_stream => sub {
+ my ( $stream ) = @_;
+
+ $stream->configure(
+ on_read => sub {
+ my ( $self, $buffref, $eof ) = @_;
+
+ while( $$buffref =~ s/^(.*\n)// ) {
+ print "Received a line $1";
+ }
+
+ return 0;
+ }
+ );
+
+ $stream->write( "An initial line here\n" );
+
+ $loop->add( $stream );
+ },
+
+ on_resolve_error => sub { die "Cannot resolve - $_[-1]\n"; },
+ on_connect_error => sub { die "Cannot connect - $_[0] failed $_[-1]\n"; },
+ );
+
+ $loop->run;
+
+DESCRIPTION
+ This collection of modules allows programs to be written that perform
+ asynchronous filehandle IO operations. A typical program using them
+ would consist of a single subclass of IO::Async::Loop to act as a
+ container of other objects, which perform the actual IO work required by
+ the program. As well as IO handles, the loop also supports timers and
+ signal handlers, and includes more higher-level functionality built on
+ top of these basic parts.
+
+ Because there are a lot of classes in this collection, the following
+ overview gives a brief description of each.
+
+ Notifiers
+ The base class of all the event handling subclasses is
+ IO::Async::Notifier. It does not perform any IO operations itself, but
+ instead acts as a base class to build the specific IO functionality
+ upon. It can also coordinate a collection of other Notifiers contained
+ within it, forming a tree structure.
+
+ The following sections describe particular types of Notifier.
+
+ File Handle IO
+ An IO::Async::Handle object is a Notifier that represents a single IO
+ handle being managed. While in most cases it will represent a single
+ filehandle, such as a socket (for example, an IO::Socket::INET
+ connection), it is possible to have separate reading and writing handles
+ (most likely for a program's `STDIN' and `STDOUT' streams, or a pair of
+ pipes connected to a child process).
+
+ The IO::Async::Stream class is a subclass of IO::Async::Handle which
+ maintains internal incoming and outgoing data buffers. In this way, it
+ implements bidirectional buffering of a byte stream, such as a TCP
+ socket. The class automatically handles reading of incoming data into
+ the incoming buffer, and writing of the outgoing buffer. Methods or
+ callbacks are used to inform when new incoming data is available, or
+ when the outgoing buffer is empty.
+
+ While stream-based sockets can be handled using using
+ `IO::Async::Stream', datagram or raw sockets do not provide a
+ bytestream. For these, the IO::Async::Socket class is another subclass
+ of IO::Async::Handle which maintains an outgoing packet queue, and
+ informs of packet receipt using a callback or method.
+
+ The IO::Async::Listener class is another subclass of IO::Async::Handle
+ which facilitates the use of `listen(2)'-mode sockets. When a new
+ connection is available on the socket it will `accept(2)' it and pass
+ the new client socket to its callback function.
+
+ Timers
+ An IO::Async::Timer::Absolute object represents a timer that expires at
+ a given absolute time in the future.
+
+ An IO::Async::Timer::Countdown object represents a count time timer,
+ which will invoke a callback after a given delay. It can be stopped and
+ restarted.
+
+ An IO::Async::Timer::Periodic object invokes a callback at regular
+ intervals from its initial start time. It is reliable and will not drift
+ due to the time taken to run the callback.
+
+ The IO::Async::Loop also supports methods for managing timed events on a
+ lower level. Events may be absolute, or relative in time to the time
+ they are installed.
+
+ Signals
+ An IO::Async::Signal object represents a POSIX signal, which will invoke
+ a callback when the given signal is received by the process. Multiple
+ objects watching the same signal can be used; they will all invoke in no
+ particular order.
+
+ Processes Management
+ An IO::Async::PID object invokes its event when a given child process
+ exits. An IO::Async::Process object can start a new child process
+ running either a given block of code, or executing a given command, set
+ up pipes on its filehandles, write to or read from these pipes, and
+ invoke its event when the child process exits.
+
+ Loops
+ The IO::Async::Loop object class represents an abstract collection of
+ IO::Async::Notifier objects, and manages the actual filehandle IO
+ watchers, timers, signal handlers, and other functionality. It performs
+ all of the abstract collection management tasks, and leaves the actual
+ OS interactions to a particular subclass for the purpose.
+
+ IO::Async::Loop::Poll uses an IO::Poll object for this test.
+
+ IO::Async::Loop::Select uses the `select(2)' syscall.
+
+ Other subclasses of loop may appear on CPAN under their own dists; see
+ the SEE ALSO section below for more detail.
+
+ As well as these general-purpose classes, the IO::Async::Loop
+ constructor also supports looking for OS-specific subclasses, in case a
+ more efficient implementation exists for the specific OS it runs on.
+
+ Child Processes
+ The IO::Async::Loop object provides a number of methods to facilitate
+ the running of child processes. `spawn_child' is primarily a wrapper
+ around the typical `fork(2)'/`exec(2)' style of starting child
+ processes, and `run_child' provide a method similar to perl's `readpipe'
+ (which is used to implement backticks ```').
+
+ File Change Watches
+ The IO::Async::File object observes changes to `stat(2)' properties of a
+ file, directory, or other filesystem object. It invokes callbacks when
+ properties change. This is used by IO::Async::FileStream which presents
+ the same events as a `IO::Async::Stream' but operates on a regular file
+ on the filesystem, observing it for updates.
+
+ Asynchronous Co-routines and Functions
+ The `IO::Async' framework generally provides mechanisms for multiplexing
+ IO tasks between different handles, so there aren't many occasions when
+ it is necessary to run code in another thread or process. Two cases
+ where this does become useful are when:
+
+ * A large amount of computationally-intensive work needs to be
+ performed.
+
+ * An OS or library-level function needs to be called, that will block,
+ and no asynchronous version is supplied.
+
+ For these cases, an instance of IO::Async::Function can be used around a
+ code block, to execute it in a worker child process or set of processes.
+ The code in the sub-process runs isolated from the main program,
+ communicating only by function call arguments and return values. This
+ can be used to solve problems involving state-less library functions.
+
+ An IO::Async::Routine object wraps a code block running in a separate
+ process to form a kind of co-routine. Communication with it happens via
+ IO::Async::Channel objects. It can be used to solve any sort of problem
+ involving keeping a possibly-stateful co-routine running alongside the
+ rest of an asynchronous program.
+
+ Futures
+ An IO::Async::Future object represents a single outstanding action that
+ is yet to complete, such as a name resolution operation or a socket
+ connection. It stands in contrast to a `IO::Async::Notifier', which is
+ an object that represents an ongoing source of activity, such as a
+ readable filehandle of bytes or a POSIX signal.
+
+ Futures are a recent addition to the `IO::Async' API and details are
+ still subject to change and experimentation.
+
+ In general, methods that support Futures return a new Future object to
+ represent the outstanding operation. If callback functions are supplied
+ as well, these will be fired in addition to the Future object becoming
+ ready. Any failures that are reported will, in general, use the same
+ conventions for the Future's `fail' arguments to relate it to the legacy
+ `on_error'-style callbacks.
+
+ $on_NAME_error->( $message, @argmuents )
+
+ $f->fail( $message, NAME, @arguments )
+
+ where `$message' is a message intended for humans to read (so that this
+ is the message displayed by `$f->get' if the failure is not otherwise
+ caught), `NAME' is the name of the failing operation. If the failure is
+ due to a failed system call, the value of `$!' will be the final
+ argument. The message should not end with a linefeed.
+
+ Networking
+ The IO::Async::Loop provides several methods for performing
+ network-based tasks. Primarily, the `connect' and `listen' methods allow
+ the creation of client or server network sockets. Additionally, the
+ `resolve' method allows the use of the system's name resolvers in an
+ asynchronous way, to resolve names into addresses, or vice versa. These
+ methods are fully IPv6-capable if the underlying operating system is.
+
+ Protocols
+ The IO::Async::Protocol class provides storage for a IO::Async::Handle
+ object, to act as a transport for some protocol. It allows a level of
+ independence from the actual transport being for that protocol, allowing
+ it to be easily reused. The IO::Async::Protocol::Stream subclass
+ provides further support for protocols based on stream connections, such
+ as TCP sockets.
+
+TODO
+ This collection of modules is still very much in development. As a
+ result, some of the potentially-useful parts or features currently
+ missing are:
+
+ * Consider further ideas on Solaris' *ports*, BSD's *Kevents* and
+ anything that might be useful on Win32.
+
+ * Consider some form of persistent object wrapper in the form of an
+ `IO::Async::Object', based on `IO::Async::Routine'.
+
+ * `IO::Async::Protocol::Datagram'
+
+ * Support for watching filesystem entries for change. Extract logic
+ from `IO::Async::File' and define a Loop watch/unwatch method pair.
+
+ * Define more `Future'-returning methods. Consider also one-shot
+ Futures on things like `IO::Async::Process' exits, or
+ `IO::Async::Handle' close.
+
+SUPPORT
+ Bugs may be reported via RT at
+
+ https://rt.cpan.org/Public/Dist/Display.html?Name=IO-Async
+
+ Support by IRC may also be found on irc.perl.org in the #io-async
+ channel.
+
+SEE ALSO
+ As well as the two loops supplied in this distribution, many more exist
+ on CPAN. At the time of writing this includes:
+
+ * IO::Async::Loop::AnyEvent - use IO::Async with AnyEvent
+
+ * IO::Async::Loop::Epoll - use IO::Async with epoll on Linux
+
+ * IO::Async::Loop::Event - use IO::Async with Event
+
+ * IO::Async::Loop::EV - use IO::Async with EV
+
+ * IO::Async::Loop::Glib - use IO::Async with Glib or GTK
+
+ * IO::Async::Loop::KQueue - use IO::Async with kqueue
+
+ * IO::Async::Loop::Mojo - use IO::Async with Mojolicious
+
+ * IO::Async::Loop::POE - use IO::Async with POE
+
+ * IO::Async::Loop::Ppoll - use IO::Async with ppoll(2)
+
+ Additionally, some other event loops or modules also support being run
+ on top of `IO::Async':
+
+ * AnyEvent::Impl::IOAsync - AnyEvent adapter for IO::Async
+
+ * Gungho::Engine::IO::Async - IO::Async Engine
+
+ * POE::Loop::IO_Async - IO::Async event loop support for POE
+
+AUTHOR
+ Paul Evans <leonerd@leonerd.org.uk>
+
diff --git a/examples/chat-server.pl b/examples/chat-server.pl
new file mode 100644
index 0000000..00522e1
--- /dev/null
+++ b/examples/chat-server.pl
@@ -0,0 +1,71 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Loop;
+use IO::Async::Stream;
+
+my $PORT = 12345;
+
+my $loop = IO::Async::Loop->new;
+
+my $listener = ChatListener->new;
+
+$loop->add( $listener );
+
+$listener->listen(
+ service => $PORT,
+ socktype => 'stream',
+)->on_done( sub {
+ my ( $listener ) = @_;
+ my $socket = $listener->read_handle;
+
+ printf STDERR "Listening on %s:%d\n", $socket->sockhost, $socket->sockport;
+})->get;
+
+$loop->run;
+
+package ChatListener;
+use base qw( IO::Async::Listener );
+
+my @clients;
+
+sub on_stream
+{
+ my $self = shift;
+ my ( $stream ) = @_;
+
+ # $socket is just an IO::Socket reference
+ my $socket = $stream->read_handle;
+ my $peeraddr = $socket->peerhost . ":" . $socket->peerport;
+
+ # Inform the others
+ $_->write( "$peeraddr joins\n" ) for @clients;
+
+ $stream->configure(
+ on_read => sub {
+ my ( $self, $buffref, $eof ) = @_;
+
+ while( $$buffref =~ s/^(.*\n)// ) {
+ # eat a line from the stream input
+
+ # Reflect it to all but the stream who wrote it
+ $_ == $self or $_->write( "$peeraddr: $1" ) for @clients;
+ }
+
+ return 0;
+ },
+
+ on_closed => sub {
+ my ( $self ) = @_;
+ @clients = grep { $_ != $self } @clients;
+
+ # Inform the others
+ $_->write( "$peeraddr leaves\n" ) for @clients;
+ },
+ );
+
+ $loop->add( $stream );
+ push @clients, $stream;
+}
diff --git a/examples/echo-server.pl b/examples/echo-server.pl
new file mode 100644
index 0000000..a63f516
--- /dev/null
+++ b/examples/echo-server.pl
@@ -0,0 +1,69 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Getopt::Long;
+
+use IO::Async::Loop;
+use IO::Async::Listener;
+
+my $PORT = 12345;
+my $FAMILY;
+my $V6ONLY;
+
+GetOptions(
+ 'port|p=i' => \$PORT,
+ '4' => sub { $FAMILY = "inet" },
+ '6' => sub { $FAMILY = "inet6" },
+ 'v6only=i' => \$V6ONLY,
+) or exit 1;
+
+my $loop = IO::Async::Loop->new;
+
+my $listener = IO::Async::Listener->new(
+ on_stream => sub {
+ my $self = shift;
+ my ( $stream ) = @_;
+
+ my $socket = $stream->read_handle;
+ my $peeraddr = $socket->peerhost . ":" . $socket->peerport;
+
+ print STDERR "Accepted new connection from $peeraddr\n";
+
+ $stream->configure(
+ on_read => sub {
+ my ( $self, $buffref, $eof ) = @_;
+
+ while( $$buffref =~ s/^(.*\n)// ) {
+ # eat a line from the stream input
+ $self->write( $1 );
+ }
+
+ return 0;
+ },
+
+ on_closed => sub {
+ print STDERR "Connection from $peeraddr closed\n";
+ },
+ );
+
+ $loop->add( $stream );
+ },
+);
+
+$loop->add( $listener );
+
+$listener->listen(
+ service => $PORT,
+ socktype => 'stream',
+ family => $FAMILY,
+ v6only => $V6ONLY,
+)->on_done( sub {
+ my ( $listener ) = @_;
+ my $socket = $listener->read_handle;
+
+ printf STDERR "Listening on %s:%d\n", $socket->sockhost, $socket->sockport;
+})->get;
+
+$loop->run;
diff --git a/examples/netcat-client.pl b/examples/netcat-client.pl
new file mode 100644
index 0000000..8926131
--- /dev/null
+++ b/examples/netcat-client.pl
@@ -0,0 +1,68 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Loop;
+use IO::Async::Stream;
+
+my $CRLF = "\x0d\x0a"; # because \r\n is not portable
+
+my $HOST = shift @ARGV or die "Need HOST";
+my $PORT = shift @ARGV or die "Need PORT";
+
+my $loop = IO::Async::Loop->new;
+
+my $socket = $loop->connect(
+ host => $HOST,
+ service => $PORT,
+ socktype => 'stream',
+)->get;
+
+# $socket is just an IO::Socket reference
+my $peeraddr = $socket->peerhost . ":" . $socket->peerport;
+
+print STDERR "Connected to $peeraddr\n";
+
+# We need to create a cross-connected pair of Streams. Can't do that
+# easily without a temporary variable
+my ( $socketstream, $stdiostream );
+
+$socketstream = IO::Async::Stream->new(
+ handle => $socket,
+
+ on_read => sub {
+ my ( undef, $buffref, $eof ) = @_;
+
+ while( $$buffref =~ s/^(.*)$CRLF// ) {
+ $stdiostream->write( $1 . "\n" );
+ }
+
+ return 0;
+ },
+
+ on_closed => sub {
+ print STDERR "Closed connection to $peeraddr\n";
+ $stdiostream->close_when_empty;
+ },
+);
+$loop->add( $socketstream );
+
+$stdiostream = IO::Async::Stream->new_for_stdio(
+ on_read => sub {
+ my ( undef, $buffref, $eof ) = @_;
+
+ while( $$buffref =~ s/^(.*)\n// ) {
+ $socketstream->write( $1 . $CRLF );
+ }
+
+ return 0;
+ },
+
+ on_closed => sub {
+ $socketstream->close_when_empty;
+ },
+);
+$loop->add( $stdiostream );
+
+$loop->await_all( $socketstream->new_close_future, $stdiostream->new_close_future );
diff --git a/examples/readwrite-futures.pl b/examples/readwrite-futures.pl
new file mode 100644
index 0000000..9ab73b2
--- /dev/null
+++ b/examples/readwrite-futures.pl
@@ -0,0 +1,17 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Loop;
+use IO::Async::Stream;
+
+my $loop = IO::Async::Loop->new;
+
+$loop->add( my $stdin = IO::Async::Stream->new_for_stdin( on_read => sub { 0 } ) );
+$loop->add( my $stdout = IO::Async::Stream->new_for_stdout );
+
+$stdout->write( sub {
+ return undef if $stdin->is_read_eof;
+ return $stdin->read_atmost( 64 * 1024 );
+})->get;
diff --git a/examples/tail-logfile.pl b/examples/tail-logfile.pl
new file mode 100644
index 0000000..1f7d6eb
--- /dev/null
+++ b/examples/tail-logfile.pl
@@ -0,0 +1,32 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Loop;
+use IO::Async::FileStream;
+
+my $FILE = shift @ARGV or die "Need FILE";
+
+my $loop = IO::Async::Loop->new;
+
+open my $fh, "<", $FILE or die "Cannot open $FILE for reading - $!";
+my $filestream = IO::Async::FileStream->new(
+ read_handle => $fh,
+ on_initial => sub {
+ my ( $self ) = @_;
+ $self->seek_to_last( "\n" );
+ },
+ on_read => sub {
+ my ( undef, $buffref ) = @_;
+
+ while( $$buffref =~ s/^(.*)\n// ) {
+ print "$FILE: $1\n";
+ }
+
+ return 0;
+ },
+);
+$loop->add( $filestream );
+
+$loop->run;
diff --git a/examples/tcp-proxy.pl b/examples/tcp-proxy.pl
new file mode 100644
index 0000000..51bd44a
--- /dev/null
+++ b/examples/tcp-proxy.pl
@@ -0,0 +1,81 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Loop;
+use IO::Async::Stream;
+use IO::Async::Listener;
+
+my $LISTEN_PORT = 12345;
+my $CONNECT_HOST = "localhost";
+my $CONNECT_PORT = 80;
+
+my $loop = IO::Async::Loop->new;
+
+my $listener = ProxyListener->new;
+
+$loop->add( $listener );
+
+$listener->listen(
+ service => $LISTEN_PORT,
+ socktype => 'stream',
+)->get;
+
+$loop->run;
+
+package ProxyListener;
+use base qw( IO::Async::Listener );
+
+sub on_stream
+{
+ my $self = shift;
+ my ( $stream1 ) = @_;
+
+ # $socket is just an IO::Socket reference
+ my $socket1 = $stream1->read_handle;
+ my $peeraddr = $socket1->peerhost . ":" . $socket1->peerport;
+
+ print STDERR "Accepted new connection from $peeraddr\n";
+
+ $loop->connect(
+ host => $CONNECT_HOST,
+ service => $CONNECT_PORT,
+
+ on_stream => sub {
+ my ( $stream2 ) = @_;
+
+ $stream1->configure(
+ on_read => sub {
+ my ( $self, $buffref, $eof ) = @_;
+ # Just copy all the data
+ $stream2->write( $$buffref ); $$buffref = "";
+ return 0;
+ },
+ on_closed => sub {
+ $stream2->close_when_empty;
+ print STDERR "Connection from $peeraddr closed\n";
+ },
+ );
+
+ $stream2->configure(
+ on_read => sub {
+ my ( $self, $buffref, $eof ) = @_;
+ # Just copy all the data
+ $stream1->write( $$buffref ); $$buffref = "";
+ return 0;
+ },
+ on_closed => sub {
+ $stream1->close_when_empty;
+ print STDERR "Connection to $CONNECT_HOST:$CONNECT_PORT closed\n";
+ },
+ );
+
+ $loop->add( $stream1 );
+ $loop->add( $stream2 );
+ },
+
+ on_resolve_error => sub { print STDERR "Cannot resolve - $_[0]\n"; },
+ on_connect_error => sub { print STDERR "Cannot connect\n"; },
+ );
+}
diff --git a/examples/whoami-server.pl b/examples/whoami-server.pl
new file mode 100644
index 0000000..f11f8ea
--- /dev/null
+++ b/examples/whoami-server.pl
@@ -0,0 +1,58 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Loop;
+use IO::Async::Stream;
+use IO::Async::Listener;
+
+my $PORT = 12345;
+
+my $loop = IO::Async::Loop->new;
+
+my $listener = IO::Async::Listener->new(
+ on_accept => sub {
+ my $self = shift;
+ my ( $socket ) = @_;
+
+ # $socket is just an IO::Socket reference
+ my $peeraddr = $socket->peerhost . ":" . $socket->peerport;
+
+ my $clientstream = IO::Async::Stream->new(
+ write_handle => $socket,
+ );
+
+ $loop->add( $clientstream );
+
+ $clientstream->write( "Your address is " . $peeraddr . "\n" );
+
+ $loop->resolver->getnameinfo(
+ addr => $socket->peername,
+
+ on_resolved => sub {
+ my ( $host, $service ) = @_;
+ $clientstream->write( "You are $host:$service\n" );
+ $clientstream->close_when_empty;
+ },
+ on_error => sub {
+ $clientstream->write( "Cannot resolve your address - $_[-1]\n" );
+ $clientstream->close_when_empty;
+ },
+ );
+ },
+);
+
+$loop->add( $listener );
+
+$listener->listen(
+ service => $PORT,
+ socktype => 'stream',
+)->on_done( sub {
+ my ( $listener ) = @_;
+ my $socket = $listener->read_handle;
+
+ printf STDERR "Listening on %s:%d\n", $socket->sockhost, $socket->sockport;
+})->get;
+
+$loop->run;
diff --git a/lib/IO/Async.pm b/lib/IO/Async.pm
new file mode 100644
index 0000000..bc2d196
--- /dev/null
+++ b/lib/IO/Async.pm
@@ -0,0 +1,366 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2008-2013 -- leonerd@leonerd.org.uk
+
+package IO::Async;
+
+use strict;
+use warnings;
+
+# This package contains no code other than a declaration of the version.
+# It is provided simply to keep CPAN happy:
+# cpan -i IO::Async
+
+our $VERSION = '0.67';
+
+=head1 NAME
+
+C<IO::Async> - Asynchronous event-driven programming
+
+=head1 SYNOPSIS
+
+ use IO::Async::Stream;
+ use IO::Async::Loop;
+
+ my $loop = IO::Async::Loop->new;
+
+ $loop->connect(
+ host => "some.other.host",
+ service => 12345,
+ socktype => 'stream',
+
+ on_stream => sub {
+ my ( $stream ) = @_;
+
+ $stream->configure(
+ on_read => sub {
+ my ( $self, $buffref, $eof ) = @_;
+
+ while( $$buffref =~ s/^(.*\n)// ) {
+ print "Received a line $1";
+ }
+
+ return 0;
+ }
+ );
+
+ $stream->write( "An initial line here\n" );
+
+ $loop->add( $stream );
+ },
+
+ on_resolve_error => sub { die "Cannot resolve - $_[-1]\n"; },
+ on_connect_error => sub { die "Cannot connect - $_[0] failed $_[-1]\n"; },
+ );
+
+ $loop->run;
+
+=head1 DESCRIPTION
+
+This collection of modules allows programs to be written that perform
+asynchronous filehandle IO operations. A typical program using them would
+consist of a single subclass of L<IO::Async::Loop> to act as a container of
+other objects, which perform the actual IO work required by the program. As
+well as IO handles, the loop also supports timers and signal handlers, and
+includes more higher-level functionality built on top of these basic parts.
+
+Because there are a lot of classes in this collection, the following overview
+gives a brief description of each.
+
+=head2 Notifiers
+
+The base class of all the event handling subclasses is L<IO::Async::Notifier>.
+It does not perform any IO operations itself, but instead acts as a base class
+to build the specific IO functionality upon. It can also coordinate a
+collection of other Notifiers contained within it, forming a tree structure.
+
+The following sections describe particular types of Notifier.
+
+=head2 File Handle IO
+
+An L<IO::Async::Handle> object is a Notifier that represents a single IO handle
+being managed. While in most cases it will represent a single filehandle, such
+as a socket (for example, an L<IO::Socket::INET> connection), it is possible
+to have separate reading and writing handles (most likely for a program's
+C<STDIN> and C<STDOUT> streams, or a pair of pipes connected to a child
+process).
+
+The L<IO::Async::Stream> class is a subclass of L<IO::Async::Handle> which
+maintains internal incoming and outgoing data buffers. In this way, it
+implements bidirectional buffering of a byte stream, such as a TCP socket. The
+class automatically handles reading of incoming data into the incoming buffer,
+and writing of the outgoing buffer. Methods or callbacks are used to inform
+when new incoming data is available, or when the outgoing buffer is empty.
+
+While stream-based sockets can be handled using using C<IO::Async::Stream>,
+datagram or raw sockets do not provide a bytestream. For these, the
+L<IO::Async::Socket> class is another subclass of L<IO::Async::Handle> which
+maintains an outgoing packet queue, and informs of packet receipt using a
+callback or method.
+
+The L<IO::Async::Listener> class is another subclass of L<IO::Async::Handle>
+which facilitates the use of C<listen(2)>-mode sockets. When a new connection
+is available on the socket it will C<accept(2)> it and pass the new client
+socket to its callback function.
+
+=head2 Timers
+
+An L<IO::Async::Timer::Absolute> object represents a timer that expires at a
+given absolute time in the future.
+
+An L<IO::Async::Timer::Countdown> object represents a count time timer, which
+will invoke a callback after a given delay. It can be stopped and restarted.
+
+An L<IO::Async::Timer::Periodic> object invokes a callback at regular intervals
+from its initial start time. It is reliable and will not drift due to the time
+taken to run the callback.
+
+The L<IO::Async::Loop> also supports methods for managing timed events on a
+lower level. Events may be absolute, or relative in time to the time they are
+installed.
+
+=head2 Signals
+
+An L<IO::Async::Signal> object represents a POSIX signal, which will invoke a
+callback when the given signal is received by the process. Multiple objects
+watching the same signal can be used; they will all invoke in no particular
+order.
+
+=head2 Processes Management
+
+An L<IO::Async::PID> object invokes its event when a given child process
+exits. An L<IO::Async::Process> object can start a new child process running
+either a given block of code, or executing a given command, set up pipes on
+its filehandles, write to or read from these pipes, and invoke its event when
+the child process exits.
+
+=head2 Loops
+
+The L<IO::Async::Loop> object class represents an abstract collection of
+L<IO::Async::Notifier> objects, and manages the actual filehandle IO
+watchers, timers, signal handlers, and other functionality. It performs all
+of the abstract collection management tasks, and leaves the actual OS
+interactions to a particular subclass for the purpose.
+
+L<IO::Async::Loop::Poll> uses an L<IO::Poll> object for this test.
+
+L<IO::Async::Loop::Select> uses the C<select(2)> syscall.
+
+Other subclasses of loop may appear on CPAN under their own dists; see the
+L</SEE ALSO> section below for more detail.
+
+As well as these general-purpose classes, the L<IO::Async::Loop> constructor
+also supports looking for OS-specific subclasses, in case a more efficient
+implementation exists for the specific OS it runs on.
+
+=head2 Child Processes
+
+The L<IO::Async::Loop> object provides a number of methods to facilitate the
+running of child processes. C<spawn_child> is primarily a wrapper around the
+typical C<fork(2)>/C<exec(2)> style of starting child processes, and
+C<run_child> provide a method similar to perl's C<readpipe> (which is used
+to implement backticks C<``>).
+
+=head2 File Change Watches
+
+The L<IO::Async::File> object observes changes to C<stat(2)> properties of a
+file, directory, or other filesystem object. It invokes callbacks when
+properties change. This is used by L<IO::Async::FileStream> which presents
+the same events as a C<IO::Async::Stream> but operates on a regular file on
+the filesystem, observing it for updates.
+
+=head2 Asynchronous Co-routines and Functions
+
+The C<IO::Async> framework generally provides mechanisms for multiplexing IO
+tasks between different handles, so there aren't many occasions when it is
+necessary to run code in another thread or process. Two cases where this does
+become useful are when:
+
+=over 4
+
+=item *
+
+A large amount of computationally-intensive work needs to be performed.
+
+=item *
+
+An OS or library-level function needs to be called, that will block, and
+no asynchronous version is supplied.
+
+=back
+
+For these cases, an instance of L<IO::Async::Function> can be used around
+a code block, to execute it in a worker child process or set of processes.
+The code in the sub-process runs isolated from the main program, communicating
+only by function call arguments and return values. This can be used to solve
+problems involving state-less library functions.
+
+An L<IO::Async::Routine> object wraps a code block running in a separate
+process to form a kind of co-routine. Communication with it happens via
+L<IO::Async::Channel> objects. It can be used to solve any sort of problem
+involving keeping a possibly-stateful co-routine running alongside the rest of
+an asynchronous program.
+
+=head2 Futures
+
+An L<IO::Async::Future> object represents a single outstanding action that is
+yet to complete, such as a name resolution operation or a socket connection.
+It stands in contrast to a C<IO::Async::Notifier>, which is an object that
+represents an ongoing source of activity, such as a readable filehandle of
+bytes or a POSIX signal.
+
+Futures are a recent addition to the C<IO::Async> API and details are still
+subject to change and experimentation.
+
+In general, methods that support Futures return a new Future object to
+represent the outstanding operation. If callback functions are supplied as
+well, these will be fired in addition to the Future object becoming ready. Any
+failures that are reported will, in general, use the same conventions for the
+Future's C<fail> arguments to relate it to the legacy C<on_error>-style
+callbacks.
+
+ $on_NAME_error->( $message, @argmuents )
+
+ $f->fail( $message, NAME, @arguments )
+
+where C<$message> is a message intended for humans to read (so that this is
+the message displayed by C<< $f->get >> if the failure is not otherwise
+caught), C<NAME> is the name of the failing operation. If the failure is due
+to a failed system call, the value of C<$!> will be the final argument. The
+message should not end with a linefeed.
+
+=head2 Networking
+
+The L<IO::Async::Loop> provides several methods for performing network-based
+tasks. Primarily, the C<connect> and C<listen> methods allow the creation of
+client or server network sockets. Additionally, the C<resolve> method allows
+the use of the system's name resolvers in an asynchronous way, to resolve
+names into addresses, or vice versa. These methods are fully IPv6-capable if
+the underlying operating system is.
+
+=head2 Protocols
+
+The L<IO::Async::Protocol> class provides storage for a L<IO::Async::Handle>
+object, to act as a transport for some protocol. It allows a level of
+independence from the actual transport being for that protocol, allowing it to
+be easily reused. The L<IO::Async::Protocol::Stream> subclass provides further
+support for protocols based on stream connections, such as TCP sockets.
+
+=head1 TODO
+
+This collection of modules is still very much in development. As a result,
+some of the potentially-useful parts or features currently missing are:
+
+=over 4
+
+=item *
+
+Consider further ideas on Solaris' I<ports>, BSD's I<Kevents> and anything that
+might be useful on Win32.
+
+=item *
+
+Consider some form of persistent object wrapper in the form of an
+C<IO::Async::Object>, based on C<IO::Async::Routine>.
+
+=item *
+
+C<IO::Async::Protocol::Datagram>
+
+=item *
+
+Support for watching filesystem entries for change. Extract logic from
+C<IO::Async::File> and define a Loop watch/unwatch method pair.
+
+=item *
+
+Define more C<Future>-returning methods. Consider also one-shot Futures on
+things like C<IO::Async::Process> exits, or C<IO::Async::Handle> close.
+
+=back
+
+=head1 SUPPORT
+
+Bugs may be reported via RT at
+
+ https://rt.cpan.org/Public/Dist/Display.html?Name=IO-Async
+
+Support by IRC may also be found on F<irc.perl.org> in the F<#io-async>
+channel.
+
+=cut
+
+=head1 SEE ALSO
+
+As well as the two loops supplied in this distribution, many more exist on
+CPAN. At the time of writing this includes:
+
+=over 4
+
+=item *
+
+L<IO::Async::Loop::AnyEvent> - use IO::Async with AnyEvent
+
+=item *
+
+L<IO::Async::Loop::Epoll> - use IO::Async with epoll on Linux
+
+=item *
+
+L<IO::Async::Loop::Event> - use IO::Async with Event
+
+=item *
+
+L<IO::Async::Loop::EV> - use IO::Async with EV
+
+=item *
+
+L<IO::Async::Loop::Glib> - use IO::Async with Glib or GTK
+
+=item *
+
+L<IO::Async::Loop::KQueue> - use IO::Async with kqueue
+
+=item *
+
+L<IO::Async::Loop::Mojo> - use IO::Async with Mojolicious
+
+=item *
+
+L<IO::Async::Loop::POE> - use IO::Async with POE
+
+=item *
+
+L<IO::Async::Loop::Ppoll> - use IO::Async with ppoll(2)
+
+=back
+
+Additionally, some other event loops or modules also support being run on top
+of C<IO::Async>:
+
+=over 4
+
+=item *
+
+L<AnyEvent::Impl::IOAsync> - AnyEvent adapter for IO::Async
+
+=item *
+
+L<Gungho::Engine::IO::Async> - IO::Async Engine
+
+=item *
+
+L<POE::Loop::IO_Async> - IO::Async event loop support for POE
+
+=back
+
+=cut
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/Channel.pm b/lib/IO/Async/Channel.pm
new file mode 100644
index 0000000..6e638f9
--- /dev/null
+++ b/lib/IO/Async/Channel.pm
@@ -0,0 +1,471 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2011-2015 -- leonerd@leonerd.org.uk
+
+package IO::Async::Channel;
+
+use strict;
+use warnings;
+use base qw( IO::Async::Notifier );
+
+our $VERSION = '0.67';
+
+use Carp;
+
+use IO::Async::Stream;
+
+=head1 NAME
+
+C<IO::Async::Channel> - pass values into or out from an L<IO::Async::Routine>
+
+=head1 DESCRIPTION
+
+A C<IO::Async::Channel> object allows Perl values to be passed into or out of
+an L<IO::Async::Routine>. It is intended to be used primarily with a Routine
+object rather than independently. For more detail and examples on how to use
+this object see also the documentation for L<IO::Async::Routine>.
+
+A Channel object is shared between the main process of the program and the
+process running within the Routine. In the main process it will be used in
+asynchronous mode, and in the Routine process it will be used in synchronous
+mode. In asynchronous mode all methods return immediately and use
+C<IO::Async>-style futures or callback functions. In synchronous within the
+Routine process the methods block until they are ready and may be used for
+flow-control within the routine. Alternatively, a Channel may be shared
+between two different Routine objects, and not used directly by the
+controlling program.
+
+The channel itself represents a FIFO of Perl reference values. New values may
+be put into the channel by the C<send> method in either mode. Values may be
+retrieved from it by the C<recv> method. Values inserted into the Channel are
+snapshot by the C<send> method. Any changes to referred variables will not be
+observed by the other end of the Channel after the C<send> method returns.
+
+=head1 PARAMETERS
+
+The following named parameters may be passed to C<new> or C<configure>:
+
+=head2 codec => STR
+
+Gives the name of the encoding method used to represent values over the
+channel.
+
+By default this will be C<Storable>, to use the core L<Storable> module. As
+this only supports references, to pass a single scalar value, C<send> a SCALAR
+reference to it, and dereference the result of C<recv>.
+
+If the L<Sereal::Encoder> and L<Sereal::Decoder> modules are installed, this
+can be set to C<Sereal> instead, and will use those to perform the encoding
+and decoding. This optional dependency may give higher performance than using
+C<Storable>.
+
+=cut
+
+=head1 CONSTRUCTOR
+
+=cut
+
+=head2 $channel = IO::Async::Channel->new
+
+Returns a new C<IO::Async::Channel> object. This object reference itself
+should be shared by both sides of a C<fork()>ed process. After C<fork()> the
+two C<setup_*> methods may be used to configure the object for operation on
+either end.
+
+While this object does in fact inherit from L<IO::Async::Notifier>, it should
+not be added to a Loop object directly; event management will be handled by
+its containing C<IO::Async::Routine> object.
+
+=cut
+
+=head1 METHODS
+
+The following methods documented with a trailing call to C<< ->get >> return
+L<Future> instances.
+
+=cut
+
+=head2 $channel->configure( %params )
+
+Similar to the standard C<configure> method on C<IO::Async::Notifier>, this is
+used to change details of the Channel's operation.
+
+=over 4
+
+=item on_recv => CODE
+
+May only be set on an async mode channel. If present, will be invoked whenever
+a new value is received, rather than using the C<recv> method.
+
+ $on_recv->( $channel, $data )
+
+=item on_eof => CODE
+
+May only be set on an async mode channel. If present, will be invoked when the
+channel gets closed by the peer.
+
+ $on_eof->( $channel )
+
+=back
+
+=cut
+
+sub _init
+{
+ my $self = shift;
+ my ( $params ) = @_;
+
+ $params->{codec} //= "Storable";
+
+ $self->SUPER::_init( $params );
+}
+
+sub configure
+{
+ my $self = shift;
+ my %params = @_;
+
+ foreach (qw( on_recv on_eof )) {
+ next unless exists $params{$_};
+ $self->{mode} and $self->{mode} eq "async" or
+ croak "Can only configure $_ in async mode";
+
+ $self->{$_} = delete $params{$_};
+ $self->_build_stream;
+ }
+
+ if( my $codec = delete $params{codec} ) {
+ ( $self->can( "_make_codec_$codec" ) or croak "Unrecognised codec name '$codec'" )
+ ->( $self );
+ }
+
+ $self->SUPER::configure( %params );
+}
+
+sub _make_codec_Storable
+{
+ my $self = shift;
+
+ require Storable;
+
+ $self->{encode} = \&Storable::freeze;
+ $self->{decode} = \&Storable::thaw;
+}
+
+sub _make_codec_Sereal
+{
+ my $self = shift;
+
+ require Sereal::Encoder;
+ require Sereal::Decoder;
+
+ my $encoder = Sereal::Encoder->new;
+ $self->{encode} = sub { $encoder->encode( $_[0] ) };
+
+ my $decoder = Sereal::Decoder->new;
+ $self->{decode} = sub { $decoder->decode( $_[0] ) };
+}
+
+=head2 $channel->send( $data )
+
+Pushes the data stored in the given Perl reference into the FIFO of the
+Channel, where it can be received by the other end. When called on a
+synchronous mode Channel this method may block if a C<write()> call on the
+underlying filehandle blocks. When called on an asynchronous mode channel this
+method will not block.
+
+=cut
+
+sub send
+{
+ my $self = shift;
+ my ( $data ) = @_;
+
+ $self->send_frozen( $self->{encode}->( $data ) );
+}
+
+=head2 $channel->send_frozen( $record )
+
+A variant of the C<send> method; this method pushes the byte record given.
+This should be the result of a call to C<Storable::freeze()>.
+
+=cut
+
+sub send_frozen
+{
+ my $self = shift;
+ my ( $record ) = @_;
+
+ my $bytes = pack( "I", length $record ) . $record;
+
+ defined $self->{mode} or die "Cannot ->send without being set up";
+
+ return $self->_send_sync( $bytes ) if $self->{mode} eq "sync";
+ return $self->_send_async( $bytes ) if $self->{mode} eq "async";
+}
+
+=head2 $data = $channel->recv
+
+When called on a synchronous mode Channel this method will block until a Perl
+reference value is available from the other end and then return it. If the
+Channel is closed this method will return C<undef>. Since only references may
+be passed and all Perl references are true the truth of the result of this
+method can be used to detect that the channel is still open and has not yet
+been closed.
+
+=head2 $data = $channel->recv->get
+
+When called on an asynchronous mode Channel this method returns a future which
+will eventually yield the next Perl reference value that becomes available
+from the other end. If the Channel is closed, the future will fail with an
+C<eof> failure.
+
+=head2 $channel->recv( %args )
+
+When not returning a future, takes the following named arguments:
+
+=over 8
+
+=item on_recv => CODE
+
+Called when a new Perl reference value is available. Will be passed the
+Channel object and the reference data.
+
+ $on_recv->( $channel, $data )
+
+=item on_eof => CODE
+
+Called if the Channel was closed before a new value was ready. Will be passed
+the Channel object.
+
+ $on_eof->( $channel )
+
+=back
+
+=cut
+
+sub recv
+{
+ my $self = shift;
+
+ defined $self->{mode} or die "Cannot ->recv without being set up";
+
+ return $self->_recv_sync( @_ ) if $self->{mode} eq "sync";
+ return $self->_recv_async( @_ ) if $self->{mode} eq "async";
+}
+
+=head2 $channel->close
+
+Closes the channel. Causes a pending C<recv> on the other end to return undef
+or the queued C<on_eof> callbacks to be invoked.
+
+=cut
+
+sub close
+{
+ my $self = shift;
+
+ return $self->_close_sync if $self->{mode} eq "sync";
+ return $self->_close_async if $self->{mode} eq "async";
+}
+
+# Leave this undocumented for now
+sub setup_sync_mode
+{
+ my $self = shift;
+ ( $self->{fh} ) = @_;
+
+ $self->{mode} = "sync";
+
+ # Since we're communicating binary structures and not Unicode text we need to
+ # enable binmode
+ binmode $self->{fh};
+
+ $self->{fh}->autoflush(1);
+}
+
+sub _read_exactly
+{
+ $_[1] = "";
+
+ while( length $_[1] < $_[2] ) {
+ my $n = read( $_[0], $_[1], $_[2]-length $_[1], length $_[1] );
+ defined $n or return undef;
+ $n or return "";
+ }
+
+ return $_[2];
+}
+
+sub _recv_sync
+{
+ my $self = shift;
+
+ my $n = _read_exactly( $self->{fh}, my $lenbuffer, 4 );
+ defined $n or die "Cannot read - $!";
+ length $n or return undef;
+
+ my $len = unpack( "I", $lenbuffer );
+
+ $n = _read_exactly( $self->{fh}, my $record, $len );
+ defined $n or die "Cannot read - $!";
+ length $n or return undef;
+
+ return $self->{decode}->( $record );
+}
+
+sub _send_sync
+{
+ my $self = shift;
+ my ( $bytes ) = @_;
+ $self->{fh}->print( $bytes );
+}
+
+sub _close_sync
+{
+ my $self = shift;
+ $self->{fh}->close;
+}
+
+# Leave this undocumented for now
+sub setup_async_mode
+{
+ my $self = shift;
+ my %args = @_;
+
+ exists $args{$_} and $self->{$_} = delete $args{$_} for qw( read_handle write_handle );
+
+ keys %args and croak "Unrecognised keys for setup_async_mode: " . join( ", ", keys %args );
+
+ $self->{mode} = "async";
+}
+
+sub _build_stream
+{
+ my $self = shift;
+ return $self->{stream} ||= do {
+ $self->{on_result_queue} = [];
+
+ my $stream = IO::Async::Stream->new(
+ read_handle => $self->{read_handle},
+ write_handle => $self->{write_handle},
+ autoflush => 1,
+ on_read => $self->_capture_weakself( '_on_stream_read' )
+ );
+
+ $self->add_child( $stream );
+
+ $stream;
+ };
+}
+
+sub _send_async
+{
+ my $self = shift;
+ my ( $bytes ) = @_;
+ $self->_build_stream->write( $bytes );
+}
+
+sub _recv_async
+{
+ my $self = shift;
+ my %args = @_;
+
+ my $on_recv = $args{on_recv};
+ my $on_eof = $args{on_eof};
+
+ my $stream = $self->_build_stream;
+
+ my $f;
+ $f = $stream->loop->new_future unless !defined wantarray;
+
+ push @{ $self->{on_result_queue} }, sub {
+ my ( $self, $type, $result ) = @_;
+ if( $type eq "recv" ) {
+ $f->done( $result ) if $f and !$f->is_cancelled;
+ $on_recv->( $self, $result ) if $on_recv;
+ }
+ else {
+ $f->fail( "EOF waiting for Channel recv", eof => ) if $f and !$f->is_cancelled;
+ $on_eof->( $self ) if $on_eof;
+ }
+ };
+
+ return $f;
+}
+
+sub _close_async
+{
+ my $self = shift;
+ if( my $stream = $self->{stream} ) {
+ $stream->close_when_empty;
+ }
+ else {
+ $_ and $_->close for $self->{read_handle}, $self->{write_handle};
+ }
+
+ undef $_ for $self->{read_handle}, $self->{write_handle};
+}
+
+sub _on_stream_read
+{
+ my $self = shift or return;
+ my ( $stream, $buffref, $eof ) = @_;
+
+ if( $eof ) {
+ while( my $on_result = shift @{ $self->{on_result_queue} } ) {
+ $on_result->( $self, eof => );
+ }
+ $self->{on_eof}->( $self ) if $self->{on_eof};
+ return;
+ }
+
+ return 0 unless length( $$buffref ) >= 4;
+ my $len = unpack( "I", $$buffref );
+ return 0 unless length( $$buffref ) >= 4 + $len;
+
+ my $record = $self->{decode}->( substr( $$buffref, 4, $len ) );
+ substr( $$buffref, 0, 4 + $len ) = "";
+
+ if( my $on_result = shift @{ $self->{on_result_queue} } ) {
+ $on_result->( $self, recv => $record );
+ }
+ else {
+ $self->{on_recv}->( $self, $record );
+ }
+
+ return 1;
+}
+
+sub _extract_read_handle
+{
+ my $self = shift;
+
+ return undef if !$self->{mode};
+
+ croak "Cannot extract filehandle" if $self->{mode} ne "async";
+ $self->{mode} = "dead";
+
+ return $self->{read_handle};
+}
+
+sub _extract_write_handle
+{
+ my $self = shift;
+
+ return undef if !$self->{mode};
+
+ croak "Cannot extract filehandle" if $self->{mode} ne "async";
+ $self->{mode} = "dead";
+
+ return $self->{write_handle};
+}
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/ChildManager.pm b/lib/IO/Async/ChildManager.pm
new file mode 100644
index 0000000..48ac108
--- /dev/null
+++ b/lib/IO/Async/ChildManager.pm
@@ -0,0 +1,705 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2007-2014 -- leonerd@leonerd.org.uk
+
+package IO::Async::ChildManager;
+
+use strict;
+use warnings;
+
+our $VERSION = '0.67';
+
+# Not a notifier
+
+use IO::Async::Stream;
+
+use IO::Async::OS;
+
+use Carp;
+use Scalar::Util qw( weaken );
+
+use POSIX qw( _exit dup dup2 nice );
+
+use constant LENGTH_OF_I => length( pack( "I", 0 ) );
+
+=head1 NAME
+
+C<IO::Async::ChildManager> - facilitates the execution of child processes
+
+=head1 SYNOPSIS
+
+This object is used indirectly via an C<IO::Async::Loop>:
+
+ use IO::Async::Loop;
+
+ my $loop = IO::Async::Loop->new;
+
+ ...
+
+ $loop->run_child(
+ command => "/bin/ps",
+
+ on_finish => sub {
+ my ( $pid, $exitcode, $stdout, $stderr ) = @_;
+ my $status = ( $exitcode >> 8 );
+ print "ps [PID $pid] exited with status $status\n";
+ },
+ );
+
+ $loop->open_child(
+ command => [ "/bin/ping", "-c4", "some.host" ],
+
+ stdout => {
+ on_read => sub {
+ my ( $stream, $buffref, $eof ) = @_;
+ while( $$buffref =~ s/^(.*)\n// ) {
+ print "PING wrote: $1\n";
+ }
+ return 0;
+ },
+ },
+
+ on_finish => sub {
+ my ( $pid, $exitcode ) = @_;
+ my $status = ( $exitcode >> 8 );
+ ...
+ },
+ );
+
+ my ( $pipeRd, $pipeWr ) = IO::Async::OS->pipepair;
+ $loop->spawn_child(
+ command => "/usr/bin/my-command",
+
+ setup => [
+ stdin => [ "open", "<", "/dev/null" ],
+ stdout => $pipeWr,
+ stderr => [ "open", ">>", "/var/log/mycmd.log" ],
+ chdir => "/",
+ ]
+
+ on_exit => sub {
+ my ( $pid, $exitcode ) = @_;
+ my $status = ( $exitcode >> 8 );
+ print "Command exited with status $status\n";
+ },
+ );
+
+ $loop->spawn_child(
+ code => sub {
+ do_something; # executes in a child process
+ return 1;
+ },
+
+ on_exit => sub {
+ my ( $pid, $exitcode, $dollarbang, $dollarat ) = @_;
+ my $status = ( $exitcode >> 8 );
+ print "Child process exited with status $status\n";
+ print " OS error was $dollarbang, exception was $dollarat\n";
+ },
+ );
+
+=head1 DESCRIPTION
+
+This module extends the functionality of the containing C<IO::Async::Loop> to
+manage the execution of child processes. It acts as a central point to store
+PID values of currently-running children, and to call the appropriate
+continuation handler code when the process terminates. It provides useful
+wrapper methods that set up filehandles and other child process details, and
+to capture the child process's STDOUT and STDERR streams.
+
+=cut
+
+# Writing to variables of $> and $) have tricky ways to obtain error results
+sub setuid
+{
+ my ( $uid ) = @_;
+
+ $> = $uid; my $saved_errno = $!;
+ $> == $uid and return 1;
+
+ $! = $saved_errno;
+ return undef;
+}
+
+sub setgid
+{
+ my ( $gid ) = @_;
+
+ $) = $gid; my $saved_errno = $!;
+ $) == $gid and return 1;
+
+ $! = $saved_errno;
+ return undef;
+}
+
+sub setgroups
+{
+ my @groups = @_;
+
+ my $gid = $)+0;
+ # Put the primary GID as the first group in the supplementary list, because
+ # some operating systems ignore this position, expecting it to indeed be
+ # the primary GID.
+ # See
+ # https://rt.cpan.org/Ticket/Display.html?id=65127
+ @groups = grep { $_ != $gid } @groups;
+
+ $) = "$gid $gid " . join " ", @groups; my $saved_errno = $!;
+
+ # No easy way to detect success or failure. Just check that we have all and
+ # only the right groups
+ my %gotgroups = map { $_ => 1 } split ' ', "$)";
+
+ $! = $saved_errno;
+ $gotgroups{$_}-- or return undef for @groups;
+ keys %gotgroups or return undef;
+
+ return 1;
+}
+
+# Internal constructor
+sub new
+{
+ my $class = shift;
+ my ( %params ) = @_;
+
+ my $loop = delete $params{loop} or croak "Expected a 'loop'";
+
+ my $self = bless {
+ loop => $loop,
+ }, $class;
+
+ weaken( $self->{loop} );
+
+ return $self;
+}
+
+=head1 METHODS
+
+When active, the following methods are available on the containing C<Loop>
+object.
+
+=cut
+
+=head2 $pid = $loop->spawn_child( %params )
+
+This method creates a new child process to run a given code block or command.
+The C<%params> hash takes the following keys:
+
+=over 8
+
+=item command => ARRAY or STRING
+
+Either a reference to an array containing the command and its arguments, or a
+plain string containing the command. This value is passed into perl's
+C<exec> function.
+
+=item code => CODE
+
+A block of code to execute in the child process. It will be called in scalar
+context inside an C<eval> block.
+
+=item setup => ARRAY
+
+A reference to an array which gives file descriptors to set up in the child
+process before running the code or command. See below.
+
+=item on_exit => CODE
+
+A continuation to be called when the child processes exits. It will be invoked
+in the following way:
+
+ $on_exit->( $pid, $exitcode, $dollarbang, $dollarat )
+
+The second argument is passed the plain perl C<$?> value.
+
+=back
+
+Exactly one of the C<command> or C<code> keys must be specified.
+
+If the C<command> key is used, the given array or string is executed using the
+C<exec> function.
+
+If the C<code> key is used, the return value will be used as the C<exit(2)>
+code from the child if it returns (or 255 if it returned C<undef> or thows an
+exception).
+
+ Case | ($exitcode >> 8) | $dollarbang | $dollarat
+ --------------+------------------------+-------------+----------
+ exec succeeds | exit code from program | 0 | ""
+ exec fails | 255 | $! | ""
+ $code returns | return value | $! | ""
+ $code dies | 255 | $! | $@
+
+It is usually more convenient to use the C<open_child> method in simple cases
+where an external program is being started in order to interact with it via
+file IO, or even C<run_child> when only the final result is required, rather
+than interaction while it is running.
+
+=cut
+
+sub spawn_child
+{
+ my $self = shift;
+ my %params = @_;
+
+ my $command = delete $params{command};
+ my $code = delete $params{code};
+ my $setup = delete $params{setup};
+ my $on_exit = delete $params{on_exit};
+
+ if( %params ) {
+ croak "Unrecognised options to spawn: " . join( ",", keys %params );
+ }
+
+ defined $command and defined $code and
+ croak "Cannot pass both 'command' and 'code' to spawn";
+
+ defined $command or defined $code or
+ croak "Must pass one of 'command' or 'code' to spawn";
+
+ my @setup = defined $setup ? $self->_check_setup_and_canonicise( $setup ) : ();
+
+ my $loop = $self->{loop};
+
+ my ( $readpipe, $writepipe );
+
+ {
+ # Ensure it's FD_CLOEXEC - this is a bit more portable than manually
+ # fiddling with F_GETFL and F_SETFL (e.g. MSWin32)
+ local $^F = -1;
+
+ ( $readpipe, $writepipe ) = IO::Async::OS->pipepair or croak "Cannot pipe() - $!";
+ }
+
+ if( defined $command ) {
+ my @command = ref( $command ) ? @$command : ( $command );
+
+ $code = sub {
+ no warnings;
+ exec( @command );
+ return;
+ };
+ }
+
+ my $kid = $loop->fork(
+ code => sub {
+ # Child
+ close( $readpipe );
+ $self->_spawn_in_child( $writepipe, $code, \@setup );
+ },
+ );
+
+ # Parent
+ close( $writepipe );
+ return $self->_spawn_in_parent( $readpipe, $kid, $on_exit );
+}
+
+=head2 C<setup> array
+
+This array gives a list of file descriptor operations to perform in the child
+process after it has been C<fork(2)>ed from the parent, before running the code
+or command. It consists of name/value pairs which are ordered; the operations
+are performed in the order given.
+
+=over 8
+
+=item fdI<n> => ARRAY
+
+Gives an operation on file descriptor I<n>. The first element of the array
+defines the operation to be performed:
+
+=over 4
+
+=item [ 'close' ]
+
+The file descriptor will be closed.
+
+=item [ 'dup', $io ]
+
+The file descriptor will be C<dup2(2)>ed from the given IO handle.
+
+=item [ 'open', $mode, $file ]
+
+The file descriptor will be opened from the named file in the given mode. The
+C<$mode> string should be in the form usually given to the C<open> function;
+such as '<' or '>>'.
+
+=item [ 'keep' ]
+
+The file descriptor will not be closed; it will be left as-is.
+
+=back
+
+A non-reference value may be passed as a shortcut, where it would contain the
+name of the operation with no arguments (i.e. for the C<close> and C<keep>
+operations).
+
+=item IO => ARRAY
+
+Shortcut for passing C<fdI<n>>, where I<n> is the fileno of the IO
+reference. In this case, the key must be a reference that implements the
+C<fileno> method. This is mostly useful for
+
+ $handle => 'keep'
+
+=item fdI<n> => IO
+
+A shortcut for the C<dup> case given above.
+
+=item stdin => ...
+
+=item stdout => ...
+
+=item stderr => ...
+
+Shortcuts for C<fd0>, C<fd1> and C<fd2> respectively.
+
+=item env => HASH
+
+A reference to a hash to set as the child process's environment.
+
+Note that this will entirely set a new environment, completely replacing the
+existing one. If you want to simply add new keys or change the values of some
+keys without removing the other existing ones, you can simply copy C<%ENV>
+into the hash before setting new keys:
+
+ env => {
+ %ENV,
+ ANOTHER => "key here",
+ }
+
+=item nice => INT
+
+Change the child process's scheduling priority using C<POSIX::nice>.
+
+=item chdir => STRING
+
+Change the child process's working directory using C<chdir>.
+
+=item setuid => INT
+
+=item setgid => INT
+
+Change the child process's effective UID or GID.
+
+=item setgroups => ARRAY
+
+Change the child process's groups list, to those groups whose numbers are
+given in the ARRAY reference.
+
+On most systems, only the privileged superuser change user or group IDs.
+C<IO::Async> will B<NOT> check before detaching the child process whether
+this is the case.
+
+If setting both the primary GID and the supplementary groups list, it is
+suggested to set the primary GID first. Moreover, some operating systems may
+require that the supplementary groups list contains the primary GID.
+
+=back
+
+If no directions for what to do with C<stdin>, C<stdout> and C<stderr> are
+given, a default of C<keep> is implied. All other file descriptors will be
+closed, unless a C<keep> operation is given for them.
+
+If C<setuid> is used, be sure to place it after any other operations that
+might require superuser privileges, such as C<setgid> or opening special
+files.
+
+=cut
+
+sub _check_setup_and_canonicise
+{
+ my $self = shift;
+ my ( $setup ) = @_;
+
+ ref $setup eq "ARRAY" or croak "'setup' must be an ARRAY reference";
+
+ return () if !@$setup;
+
+ my @setup;
+
+ my $has_setgroups;
+
+ foreach my $i ( 0 .. $#$setup / 2 ) {
+ my ( $key, $value ) = @$setup[$i*2, $i*2 + 1];
+
+ # Rewrite stdin/stdout/stderr
+ $key eq "stdin" and $key = "fd0";
+ $key eq "stdout" and $key = "fd1";
+ $key eq "stderr" and $key = "fd2";
+
+ # Rewrite other filehandles
+ ref $key and eval { $key->fileno; 1 } and $key = "fd" . $key->fileno;
+
+ if( $key =~ m/^fd(\d+)$/ ) {
+ my $fd = $1;
+ my $ref = ref $value;
+
+ if( !$ref ) {
+ $value = [ $value ];
+ }
+ elsif( $ref eq "ARRAY" ) {
+ # Already OK
+ }
+ elsif( $ref eq "GLOB" or eval { $value->isa( "IO::Handle" ) } ) {
+ $value = [ 'dup', $value ];
+ }
+ else {
+ croak "Unrecognised reference type '$ref' for file descriptor $fd";
+ }
+
+ my $operation = $value->[0];
+ grep { $_ eq $operation } qw( open close dup keep ) or
+ croak "Unrecognised operation '$operation' for file descriptor $fd";
+ }
+ elsif( $key eq "env" ) {
+ ref $value eq "HASH" or croak "Expected HASH reference for 'env' setup key";
+ }
+ elsif( $key eq "nice" ) {
+ $value =~ m/^\d+$/ or croak "Expected integer for 'nice' setup key";
+ }
+ elsif( $key eq "chdir" ) {
+ # This isn't a purely watertight test, but it does guard against
+ # silly things like passing a reference - directories such as
+ # ARRAY(0x12345) are unlikely to exist
+ -d $value or croak "Working directory '$value' does not exist";
+ }
+ elsif( $key eq "setuid" ) {
+ $value =~ m/^\d+$/ or croak "Expected integer for 'setuid' setup key";
+ }
+ elsif( $key eq "setgid" ) {
+ $value =~ m/^\d+$/ or croak "Expected integer for 'setgid' setup key";
+ $has_setgroups and carp "It is suggested to 'setgid' before 'setgroups'";
+ }
+ elsif( $key eq "setgroups" ) {
+ ref $value eq "ARRAY" or croak "Expected ARRAY reference for 'setgroups' setup key";
+ m/^\d+$/ or croak "Expected integer in 'setgroups' array" for @$value;
+ $has_setgroups = 1;
+ }
+ else {
+ croak "Unrecognised setup operation '$key'";
+ }
+
+ push @setup, $key => $value;
+ }
+
+ return @setup;
+}
+
+sub _spawn_in_parent
+{
+ my $self = shift;
+ my ( $readpipe, $kid, $on_exit ) = @_;
+
+ my $loop = $self->{loop};
+
+ # We need to wait for both the errno pipe to close, and for waitpid
+ # to give us an exit code. We'll form two closures over these two
+ # variables so we can cope with those happening in either order
+
+ my $dollarbang;
+ my ( $dollarat, $length_dollarat );
+ my $exitcode;
+ my $pipeclosed = 0;
+
+ $loop->add( IO::Async::Stream->new(
+ notifier_name => "statuspipe,kid=$kid",
+
+ read_handle => $readpipe,
+
+ on_read => sub {
+ my ( $self, $buffref, $eof ) = @_;
+
+ if( !defined $dollarbang ) {
+ if( length( $$buffref ) >= 2 * LENGTH_OF_I ) {
+ ( $dollarbang, $length_dollarat ) = unpack( "II", $$buffref );
+ substr( $$buffref, 0, 2 * LENGTH_OF_I, "" );
+ return 1;
+ }
+ }
+ elsif( !defined $dollarat ) {
+ if( length( $$buffref ) >= $length_dollarat ) {
+ $dollarat = substr( $$buffref, 0, $length_dollarat, "" );
+ return 1;
+ }
+ }
+
+ if( $eof ) {
+ $dollarbang = 0 if !defined $dollarbang;
+ if( !defined $length_dollarat ) {
+ $length_dollarat = 0;
+ $dollarat = "";
+ }
+
+ $pipeclosed = 1;
+
+ if( defined $exitcode ) {
+ local $! = $dollarbang;
+ $on_exit->( $kid, $exitcode, $!, $dollarat );
+ }
+ }
+
+ return 0;
+ }
+ ) );
+
+ $loop->watch_child( $kid => sub {
+ ( my $kid, $exitcode ) = @_;
+
+ if( $pipeclosed ) {
+ local $! = $dollarbang;
+ $on_exit->( $kid, $exitcode, $!, $dollarat );
+ }
+ } );
+
+ return $kid;
+}
+
+sub _spawn_in_child
+{
+ my $self = shift;
+ my ( $writepipe, $code, $setup ) = @_;
+
+ my $exitvalue = eval {
+ # Map of which handles will be in use by the end
+ my %fd_in_use = ( 0 => 1, 1 => 1, 2 => 1 ); # Keep STDIN, STDOUT, STDERR
+
+ # Count of how many times we'll need to use the current handles.
+ my %fds_refcount = %fd_in_use;
+
+ # To dup2() without clashes we might need to temporarily move some handles
+ my %dup_from;
+
+ my $max_fd = 0;
+ my $writepipe_clashes = 0;
+
+ if( @$setup ) {
+ # The writepipe might be in the way of a setup filedescriptor. If it
+ # is we'll have to dup2 it out of the way then close the original.
+ foreach my $i ( 0 .. $#$setup/2 ) {
+ my ( $key, $value ) = @$setup[$i*2, $i*2 + 1];
+ $key =~ m/^fd(\d+)$/ or next;
+ my $fd = $1;
+
+ $max_fd = $fd if $fd > $max_fd;
+ $writepipe_clashes = 1 if $fd == fileno $writepipe;
+
+ my ( $operation, @params ) = @$value;
+
+ $operation eq "close" and do {
+ delete $fd_in_use{$fd};
+ delete $fds_refcount{$fd};
+ };
+
+ $operation eq "dup" and do {
+ $fd_in_use{$fd} = 1;
+
+ my $fileno = fileno $params[0];
+ # Keep a count of how many times it will be dup'ed from so we
+ # can close it once we've finished
+ $fds_refcount{$fileno}++;
+
+ $dup_from{$fileno} = $fileno;
+ };
+
+ $operation eq "keep" and do {
+ $fds_refcount{$fd} = 1;
+ };
+ }
+ }
+
+ foreach ( IO::Async::OS->potentially_open_fds ) {
+ next if $fds_refcount{$_};
+ next if $_ == fileno $writepipe;
+ POSIX::close( $_ );
+ }
+
+ if( @$setup ) {
+ if( $writepipe_clashes ) {
+ $max_fd++;
+
+ dup2( fileno $writepipe, $max_fd ) or die "Cannot dup2(writepipe to $max_fd) - $!\n";
+ undef $writepipe;
+ open( $writepipe, ">&=$max_fd" ) or die "Cannot fdopen($max_fd) as writepipe - $!\n";
+ }
+
+ foreach my $i ( 0 .. $#$setup/2 ) {
+ my ( $key, $value ) = @$setup[$i*2, $i*2 + 1];
+
+ if( $key =~ m/^fd(\d+)$/ ) {
+ my $fd = $1;
+ my( $operation, @params ) = @$value;
+
+ $operation eq "dup" and do {
+ my $from = fileno $params[0];
+
+ if( $from != $fd ) {
+ if( exists $dup_from{$fd} ) {
+ defined( $dup_from{$fd} = dup( $fd ) ) or die "Cannot dup($fd) - $!";
+ }
+
+ my $real_from = $dup_from{$from};
+
+ POSIX::close( $fd );
+ dup2( $real_from, $fd ) or die "Cannot dup2($real_from to $fd) - $!\n";
+ }
+
+ $fds_refcount{$from}--;
+ if( !$fds_refcount{$from} and !$fd_in_use{$from} ) {
+ POSIX::close( $from );
+ delete $dup_from{$from};
+ }
+ };
+
+ $operation eq "open" and do {
+ my ( $mode, $filename ) = @params;
+ open( my $fh, $mode, $filename ) or die "Cannot open('$mode', '$filename') - $!\n";
+
+ my $from = fileno $fh;
+ dup2( $from, $fd ) or die "Cannot dup2($from to $fd) - $!\n";
+
+ close $fh;
+ };
+ }
+ elsif( $key eq "env" ) {
+ %ENV = %$value;
+ }
+ elsif( $key eq "nice" ) {
+ nice( $value ) or die "Cannot nice($value) - $!";
+ }
+ elsif( $key eq "chdir" ) {
+ chdir( $value ) or die "Cannot chdir('$value') - $!";
+ }
+ elsif( $key eq "setuid" ) {
+ setuid( $value ) or die "Cannot setuid('$value') - $!";
+ }
+ elsif( $key eq "setgid" ) {
+ setgid( $value ) or die "Cannot setgid('$value') - $!";
+ }
+ elsif( $key eq "setgroups" ) {
+ setgroups( @$value ) or die "Cannot setgroups() - $!";
+ }
+ }
+ }
+
+ $code->();
+ };
+
+ my $writebuffer = "";
+ $writebuffer .= pack( "I", $!+0 );
+ $writebuffer .= pack( "I", length( $@ ) ) . $@;
+
+ syswrite( $writepipe, $writebuffer );
+
+ return $exitvalue;
+}
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/Debug.pm b/lib/IO/Async/Debug.pm
new file mode 100644
index 0000000..b23db0e
--- /dev/null
+++ b/lib/IO/Async/Debug.pm
@@ -0,0 +1,98 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2015 -- leonerd@leonerd.org.uk
+
+package IO::Async::Debug;
+
+use strict;
+use warnings;
+
+our $VERSION = '0.67';
+
+our $DEBUG = $ENV{IO_ASYNC_DEBUG} || 0;
+our $DEBUG_FD = $ENV{IO_ASYNC_DEBUG_FD};
+our $DEBUG_FILE = $ENV{IO_ASYNC_DEBUG_FILE};
+our $DEBUG_FH;
+our %DEBUG_FLAGS = map { $_ => 1 } split m/,/, $ENV{IO_ASYNC_DEBUG_FLAGS} // "";
+
+=head1 NAME
+
+C<IO::Async::Debug> - debugging control and support for L<IO::Async>
+
+=head1 DESCRIPTION
+
+The following methods and behaviours are still experimental and may change or
+even be removed in future.
+
+Debugging support is enabled by an environment variable called
+C<IO_ASYNC_DEBUG> having a true value.
+
+When debugging is enabled, the C<make_event_cb> and C<invoke_event> methods
+on L<IO::Async::Notifier> (and their C<maybe_> variants) are altered such that
+when the event is fired, a debugging line is printed, using the C<debug_printf>
+method. This identifes the name of the event.
+
+By default, the line is only printed if the caller of one of these methods is
+the same package as the object is blessed into, allowing it to print the
+events of the most-derived class, without the extra verbosity of the
+lower-level events of its parent class used to create it. All calls regardless
+of caller can be printed by setting a number greater than 1 as the value of
+C<IO_ASYNC_DEBUG>.
+
+By default the debugging log goes to C<STDERR>, but two other environment
+variables can redirect it. If C<IO_ASYNC_DEBUG_FILE> is set, it names a file
+which will be opened for writing, and logging written into it. Otherwise, if
+C<IO_ASYNC_DEBUG_FD> is set, it gives a file descriptor number that logging
+should be written to. If opening the named file or file descriptor fails then
+the log will be written to C<STDERR> as normal.
+
+Extra debugging flags can be set in a comma-separated list in an environment
+variable called C<IO_ASYNC_DEBUG_FLAGS>. The presence of these flags can cause
+extra information to be written to the log. Full details on these flags will
+be documented by the implementing classes. Typically these flags take the form
+of one or more capital letters indicating the class, followed by one or more
+lowercase letters enabling some particular feature within that class.
+
+=cut
+
+sub logf
+{
+ my ( $fmt, @args ) = @_;
+
+ $DEBUG_FH ||= do {
+ my $fh;
+ if( $DEBUG_FILE ) {
+ open $fh, ">", $DEBUG_FILE or undef $fh;
+ }
+ elsif( $DEBUG_FD ) {
+ $fh = IO::Handle->new;
+ $fh->fdopen( $DEBUG_FD, "w" ) or undef $fh;
+ }
+ $fh ||= \*STDERR;
+ $fh->autoflush;
+ $fh;
+ };
+
+ printf $DEBUG_FH $fmt, @args;
+}
+
+sub log_hexdump
+{
+ my ( $bytes ) = @_;
+
+ foreach my $chunk ( $bytes =~ m/(.{1,16})/sg ) {
+ my $chunk_hex = join " ", map { sprintf "%02X", ord $_ } split //, $chunk;
+ ( my $chunk_safe = $chunk ) =~ s/[^\x20-\x7e]/./g;
+
+ logf " | %-48s | %-16s |\n", $chunk_hex, $chunk_safe;
+ }
+}
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/File.pm b/lib/IO/Async/File.pm
new file mode 100644
index 0000000..cbb3604
--- /dev/null
+++ b/lib/IO/Async/File.pm
@@ -0,0 +1,219 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2012 -- leonerd@leonerd.org.uk
+
+package IO::Async::File;
+
+use 5.010; # //
+use strict;
+use warnings;
+
+our $VERSION = '0.67';
+
+use base qw( IO::Async::Timer::Periodic );
+
+use Carp;
+use File::stat;
+
+# No point watching blksize or blocks
+my @STATS = qw( dev ino mode nlink uid gid rdev size atime mtime ctime );
+
+=head1 NAME
+
+C<IO::Async::File> - watch a file for changes
+
+=head1 SYNOPSIS
+
+ use IO::Async::File;
+
+ use IO::Async::Loop;
+ my $loop = IO::Async::Loop->new;
+
+ my $file = IO::Async::File->new(
+ filename => "config.ini",
+ on_mtime_changed => sub {
+ my ( $self ) = @_;
+ print STDERR "Config file has changed\n";
+ reload_config( $self->handle );
+ }
+ );
+
+ $loop->add( $file );
+
+ $loop->run;
+
+=head1 DESCRIPTION
+
+This subclass of L<IO::Async::Notifier> watches an open filehandle or named
+filesystem entity for changes in its C<stat()> fields. It invokes various
+events when the values of these fields change. It is most often used to watch
+a file for size changes; for this task see also L<IO::Async::FileStream>.
+
+While called "File", it is not required that the watched filehandle be a
+regular file. It is possible to watch anything that C<stat(2)> may be called
+on, such as directories or other filesystem entities.
+
+=cut
+
+=head1 EVENTS
+
+The following events are invoked, either using subclass methods or CODE
+references in parameters.
+
+=head2 on_dev_changed $new_dev, $old_dev
+
+=head2 on_ino_changed $new_ino, $old_ino
+
+=head2 ...
+
+=head2 on_ctime_changed $new_ctime, $old_ctime
+
+Invoked when each of the individual C<stat()> fields have changed. All the
+C<stat()> fields are supported apart from C<blocks> and C<blksize>. Each is
+passed the new and old values of the field.
+
+=head2 on_devino_changed $new_stat, $old_stat
+
+Invoked when either of the C<dev> or C<ino> fields have changed. It is passed
+two L<File::stat> instances containing the complete old and new C<stat()>
+fields. This can be used to observe when a named file is renamed; it will not
+be observed to happen on opened filehandles.
+
+=head2 on_stat_changed $new_stat, $old_stat
+
+Invoked when any of the C<stat()> fields have changed. It is passed two
+L<File::stat> instances containing the old and new C<stat()> fields.
+
+=cut
+
+=head1 PARAMETERS
+
+The following named parameters may be passed to C<new> or C<configure>.
+
+=head2 handle => IO
+
+The opened filehandle to watch for C<stat()> changes if C<filename> is not
+supplied.
+
+=head2 filename => STRING
+
+Optional. If supplied, watches the named file rather than the filehandle given
+in C<handle>. The file will be opened for reading and then watched for
+renames. If the file is renamed, the new filename is opened and tracked
+similarly after closing the previous file.
+
+=head2 interval => NUM
+
+Optional. The interval in seconds to poll the filehandle using C<stat(2)>
+looking for size changes. A default of 2 seconds will be applied if not
+defined.
+
+=cut
+
+sub _init
+{
+ my $self = shift;
+ my ( $params ) = @_;
+
+ $params->{interval} ||= 2;
+
+ $self->SUPER::_init( $params );
+
+ $self->start;
+}
+
+sub configure
+{
+ my $self = shift;
+ my %params = @_;
+
+ if( exists $params{filename} ) {
+ my $filename = delete $params{filename};
+ $self->{filename} = $filename;
+ $self->_reopen_file;
+ }
+ elsif( exists $params{handle} ) {
+ $self->{handle} = delete $params{handle};
+ $self->{last_stat} = stat $self->{handle};
+ }
+
+ foreach ( @STATS, "devino", "stat" ) {
+ $self->{"on_${_}_changed"} = delete $params{"on_${_}_changed"} if exists $params{"on_${_}_changed"};
+ }
+
+ $self->SUPER::configure( %params );
+}
+
+sub _add_to_loop
+{
+ my $self = shift;
+
+ if( !defined $self->{filename} and !defined $self->{handle} ) {
+ croak "IO::Async::File needs either a filename or a handle";
+ }
+
+ return $self->SUPER::_add_to_loop( @_ );
+}
+
+sub _reopen_file
+{
+ my $self = shift;
+
+ my $path = $self->{filename};
+
+ open $self->{handle}, "<", $path or croak "Cannot open $path for reading - $!";
+
+ $self->{last_stat} = stat $self->{handle};
+}
+
+sub on_tick
+{
+ my $self = shift;
+
+ my $old = $self->{last_stat};
+ my $new = stat( $self->{filename} // $self->{handle} );
+
+ my $any_changed;
+ foreach my $stat ( @STATS ) {
+ next if $old->$stat == $new->$stat;
+
+ $any_changed++;
+ $self->maybe_invoke_event( "on_${stat}_changed", $new->$stat, $old->$stat );
+ }
+
+ if( $old->dev != $new->dev or $old->ino != $new->ino ) {
+ $self->maybe_invoke_event( on_devino_changed => $new, $old );
+ $self->_reopen_file;
+ }
+
+ if( $any_changed ) {
+ $self->maybe_invoke_event( on_stat_changed => $new, $old );
+ $self->{last_stat} = $new;
+ }
+}
+
+=head1 METHODS
+
+=cut
+
+=head2 $handle = $file->handle
+
+Returns the filehandle currently associated with the instance; either the one
+passed to the C<handle> parameter, or opened from the C<filename> parameter.
+
+=cut
+
+sub handle
+{
+ my $self = shift;
+ return $self->{handle};
+}
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/FileStream.pm b/lib/IO/Async/FileStream.pm
new file mode 100644
index 0000000..96778c9
--- /dev/null
+++ b/lib/IO/Async/FileStream.pm
@@ -0,0 +1,413 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2011-2012 -- leonerd@leonerd.org.uk
+
+package IO::Async::FileStream;
+
+use strict;
+use warnings;
+
+our $VERSION = '0.67';
+
+use base qw( IO::Async::Stream );
+
+use IO::Async::File;
+
+use Carp;
+use Fcntl qw( SEEK_SET SEEK_CUR );
+
+=head1 NAME
+
+C<IO::Async::FileStream> - read the tail of a file
+
+=head1 SYNOPSIS
+
+ use IO::Async::FileStream;
+
+ use IO::Async::Loop;
+ my $loop = IO::Async::Loop->new;
+
+ open my $logh, "<", "var/logs/daemon.log" or
+ die "Cannot open logfile - $!";
+
+ my $filestream = IO::Async::FileStream->new(
+ read_handle => $logh,
+
+ on_initial => sub {
+ my ( $self ) = @_;
+ $self->seek_to_last( "\n" );
+ },
+
+ on_read => sub {
+ my ( $self, $buffref ) = @_;
+
+ while( $$buffref =~ s/^(.*\n)// ) {
+ print "Received a line $1";
+ }
+
+ return 0;
+ },
+ );
+
+ $loop->add( $filestream );
+
+ $loop->run;
+
+=head1 DESCRIPTION
+
+This subclass of L<IO::Async::Stream> allows reading the end of a regular file
+which is being appended to by some other process. It invokes the C<on_read>
+event when more data has been added to the file.
+
+This class provides an API identical to C<IO::Async::Stream> when given a
+C<read_handle>; it should be treated similarly. In particular, it can be given
+an C<on_read> handler, or subclassed to provide an C<on_read> method, or even
+used as the C<transport> for an C<IO::Async::Protocol::Stream> object.
+
+It will not support writing.
+
+To watch a file, directory, or other filesystem entity for updates of other
+properties, such as C<mtime>, see also L<IO::Async::File>.
+
+=cut
+
+=head1 EVENTS
+
+The following events are invoked, either using subclass methods or CODE
+references in parameters.
+
+Because this is a subclass of L<IO::Async::Stream> in read-only mode, all the
+events supported by C<Stream> relating to the read handle are supported here.
+This is not a full list; see also the documentation relating to
+C<IO::Async::Stream>.
+
+=head2 $ret = on_read \$buffer, $eof
+
+Invoked when more data is available in the internal receiving buffer.
+
+Note that C<$eof> only indicates that all the data currently available in the
+file has now been read; in contrast to a regular C<IO::Async::Stream>, this
+object will not stop watching after this condition. Instead, it will continue
+watching the file for updates.
+
+=head2 on_truncated
+
+Invoked when the file size shrinks. If this happens, it is presumed that the
+file content has been replaced. Reading will then commence from the start of
+the file.
+
+=head2 on_initial $size
+
+Invoked the first time the file is looked at. It is passed the initial size of
+the file. The code implementing this method can use the C<seek> or
+C<seek_to_last> methods to set the initial read position in the file to skip
+over some initial content.
+
+This method may be useful to skip initial content in the file, if the object
+should only respond to new content added after it was created.
+
+=cut
+
+sub _init
+{
+ my $self = shift;
+ my ( $params ) = @_;
+
+ $self->SUPER::_init( $params );
+
+ $params->{close_on_read_eof} = 0;
+
+ $self->{last_size} = undef;
+
+ $self->add_child( $self->{file} = IO::Async::File->new(
+ on_devino_changed => $self->_replace_weakself( 'on_devino_changed' ),
+ on_size_changed => $self->_replace_weakself( 'on_size_changed' ),
+ ) );
+}
+
+=head1 PARAMETERS
+
+The following named parameters may be passed to C<new> or C<configure>, in
+addition to the parameters relating to reading supported by
+C<IO::Async::Stream>.
+
+=head2 filename => STRING
+
+Optional. If supplied, watches the named file rather than the filehandle given
+in C<read_handle>. The file will be opened by the constructor, and then
+watched for renames. If the file is renamed, the new filename is opened and
+tracked similarly after closing the previous file.
+
+=head2 interval => NUM
+
+Optional. The interval in seconds to poll the filehandle using C<stat(2)>
+looking for size changes. A default of 2 seconds will be applied if not
+defined.
+
+=cut
+
+sub configure
+{
+ my $self = shift;
+ my %params = @_;
+
+ foreach (qw( on_truncated on_initial )) {
+ $self->{$_} = delete $params{$_} if exists $params{$_};
+ }
+
+ foreach (qw( interval )) {
+ $self->{file}->configure( $_ => delete $params{$_} ) if exists $params{$_};
+ }
+ if( exists $params{filename} ) {
+ $self->{file}->configure( filename => delete $params{filename} );
+ $params{read_handle} = $self->{file}->handle;
+ }
+ elsif( exists $params{handle} or exists $params{read_handle} ) {
+ my $handle = delete $params{handle} // delete $params{read_handle};
+ $self->{file}->configure( handle => $handle );
+ $params{read_handle} = $self->{file}->handle;
+ }
+
+ croak "Cannot have a write_handle in a ".ref($self) if defined $params{write_handle};
+
+ $self->SUPER::configure( %params );
+
+ if( $self->read_handle and !defined $self->{last_size} ) {
+ my $size = (stat $self->read_handle)[7];
+
+ $self->{last_size} = $size;
+
+ local $self->{running_initial} = 1;
+ $self->maybe_invoke_event( on_initial => $size );
+ }
+}
+
+=head1 METHODS
+
+=cut
+
+# Replace IO::Async::Handle's implementation
+sub _watch_read
+{
+ my $self = shift;
+ my ( $want ) = @_;
+
+ if( $want ) {
+ $self->{file}->start if !$self->{file}->is_running;
+ }
+ else {
+ $self->{file}->stop;
+ }
+}
+
+sub _watch_write
+{
+ my $self = shift;
+ my ( $want ) = @_;
+
+ croak "Cannot _watch_write in " . ref($self) if $want;
+}
+
+sub on_devino_changed
+{
+ my $self = shift or return;
+
+ $self->{renamed} = 1;
+ $self->debug_printf( "read tail of old file" );
+ $self->read_more;
+}
+
+sub on_size_changed
+{
+ my $self = shift or return;
+ my ( $size ) = @_;
+
+ if( $size < $self->{last_size} ) {
+ $self->maybe_invoke_event( on_truncated => );
+ $self->{last_pos} = 0;
+ }
+
+ $self->{last_size} = $size;
+
+ $self->debug_printf( "read_more" );
+ $self->read_more;
+}
+
+sub read_more
+{
+ my $self = shift;
+
+ sysseek( $self->read_handle, $self->{last_pos}, SEEK_SET ) if defined $self->{last_pos};
+
+ $self->on_read_ready;
+
+ $self->{last_pos} = sysseek( $self->read_handle, 0, SEEK_CUR ); # == systell
+
+ if( $self->{last_pos} < $self->{last_size} ) {
+ $self->loop->later( sub { $self->read_more } );
+ }
+ elsif( $self->{renamed} ) {
+ $self->debug_printf( "reopening for rename" );
+
+ $self->{last_size} = 0;
+
+ if( $self->{last_pos} ) {
+ $self->maybe_invoke_event( on_truncated => );
+ $self->{last_pos} = 0;
+ $self->loop->later( sub { $self->read_more } );
+ }
+
+ $self->configure( read_handle => $self->{file}->handle );
+ undef $self->{renamed};
+ }
+}
+
+sub write
+{
+ carp "Cannot ->write from a ".ref($_[0]);
+}
+
+=head2 $filestream->seek( $offset, $whence )
+
+Callable only during the C<on_initial> event. Moves the read position in the
+filehandle to the given offset. C<$whence> is interpreted as for C<sysseek>,
+should be either C<SEEK_SET>, C<SEEK_CUR> or C<SEEK_END>. Will be set to
+C<SEEK_SET> if not provided.
+
+Normally this would be used to seek to the end of the file, for example
+
+ on_initial => sub {
+ my ( $self, $filesize ) = @_;
+ $self->seek( $filesize );
+ }
+
+=cut
+
+sub seek
+{
+ my $self = shift;
+ my ( $offset, $whence ) = @_;
+
+ $self->{running_initial} or croak "Cannot ->seek except during on_initial";
+
+ defined $whence or $whence = SEEK_SET;
+
+ sysseek( $self->read_handle, $offset, $whence );
+}
+
+=head2 $success = $filestream->seek_to_last( $str_pattern, %opts )
+
+Callable only during the C<on_initial> event. Attempts to move the read
+position in the filehandle to just after the last occurance of a given match.
+C<$str_pattern> may be a literal string or regexp pattern.
+
+Returns a true value if the seek was successful, or false if not. Takes the
+following named arguments:
+
+=over 8
+
+=item blocksize => INT
+
+Optional. Read the file in blocks of this size. Will take a default of 8KiB if
+not defined.
+
+=item horizon => INT
+
+Optional. Give up looking for a match after this number of bytes. Will take a
+default value of 4 times the blocksize if not defined.
+
+To force it to always search through the entire file contents, set this
+explicitly to C<0>.
+
+=back
+
+Because regular file reading happens synchronously, this entire method
+operates entirely synchronously. If the file is very large, it may take a
+while to read back through the entire contents. While this is happening no
+other events can be invoked in the process.
+
+When looking for a string or regexp match, this method appends the
+previously-read buffer to each block read from the file, in case a match
+becomes split across two reads. If C<blocksize> is reduced to a very small
+value, take care to ensure it isn't so small that a match may not be noticed.
+
+This is most likely useful for seeking after the last complete line in a
+line-based log file, to commence reading from the end, while still managing to
+capture any partial content that isn't yet a complete line.
+
+ on_initial => sub {
+ my $self = shift;
+ $self->seek_to_last( "\n" );
+ }
+
+=cut
+
+sub seek_to_last
+{
+ my $self = shift;
+ my ( $str_pattern, %opts ) = @_;
+
+ $self->{running_initial} or croak "Cannot ->seek_to_last except during on_initial";
+
+ my $offset = $self->{last_size};
+
+ my $blocksize = $opts{blocksize} || 8192;
+
+ defined $opts{horizon} or $opts{horizon} = $blocksize * 4;
+ my $horizon = $opts{horizon} ? $offset - $opts{horizon} : 0;
+ $horizon = 0 if $horizon < 0;
+
+ my $re = ref $str_pattern ? $str_pattern : qr/\Q$str_pattern\E/;
+
+ my $prev = "";
+ while( $offset > $horizon ) {
+ my $len = $blocksize;
+ $len = $offset if $len > $offset;
+ $offset -= $len;
+
+ sysseek( $self->read_handle, $offset, SEEK_SET );
+ sysread( $self->read_handle, my $buffer, $blocksize );
+
+ # TODO: If $str_pattern is a plain string this could be more efficient
+ # using rindex
+ if( () = ( $buffer . $prev ) =~ m/$re/sg ) {
+ # $+[0] will be end of last match
+ my $pos = $offset + $+[0];
+ $self->seek( $pos );
+ return 1;
+ }
+
+ $prev = $buffer;
+ }
+
+ $self->seek( $horizon );
+ return 0;
+}
+
+=head1 TODO
+
+=over 4
+
+=item *
+
+Move the actual file update watching code into C<IO::Async::Loop>, possibly as
+a new watch/unwatch method pair C<watch_file>.
+
+=item *
+
+Consider if a construction-time parameter of C<seek_to_end> or C<seek_to_last>
+might be neater than a small code block in C<on_initial>, if that turns out to
+be the only or most common form of use.
+
+=back
+
+=cut
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/Function.pm b/lib/IO/Async/Function.pm
new file mode 100644
index 0000000..adaf396
--- /dev/null
+++ b/lib/IO/Async/Function.pm
@@ -0,0 +1,667 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2011-2015 -- leonerd@leonerd.org.uk
+
+package IO::Async::Function;
+
+use strict;
+use warnings;
+
+our $VERSION = '0.67';
+
+use base qw( IO::Async::Notifier );
+use IO::Async::Timer::Countdown;
+
+use Carp;
+
+use Storable qw( freeze );
+
+=head1 NAME
+
+C<IO::Async::Function> - call a function asynchronously
+
+=head1 SYNOPSIS
+
+ use IO::Async::Function;
+
+ use IO::Async::Loop;
+ my $loop = IO::Async::Loop->new;
+
+ my $function = IO::Async::Function->new(
+ code => sub {
+ my ( $number ) = @_;
+ return is_prime( $number );
+ },
+ );
+
+ $loop->add( $function );
+
+ $function->call(
+ args => [ 123454321 ],
+ )->on_done( sub {
+ my $isprime = shift;
+ print "123454321 " . ( $isprime ? "is" : "is not" ) . " a prime number\n";
+ })->on_fail( sub {
+ print STDERR "Cannot determine if it's prime - $_[0]\n";
+ })->get;
+
+=head1 DESCRIPTION
+
+This subclass of L<IO::Async::Notifier> wraps a function body in a collection
+of worker processes, to allow it to execute independently of the main process.
+The object acts as a proxy to the function, allowing invocations to be made by
+passing in arguments, and invoking a continuation in the main process when the
+function returns.
+
+The object represents the function code itself, rather than one specific
+invocation of it. It can be called multiple times, by the C<call> method.
+Multiple outstanding invocations can be called; they will be dispatched in
+the order they were queued. If only one worker process is used then results
+will be returned in the order they were called. If multiple are used, then
+each request will be sent in the order called, but timing differences between
+each worker may mean results are returned in a different order.
+
+Since the code block will be called multiple times within the same child
+process, it must take care not to modify any of its state that might affect
+subsequent calls. Since it executes in a child process, it cannot make any
+modifications to the state of the parent program. Therefore, all the data
+required to perform its task must be represented in the call arguments, and
+all of the result must be represented in the return values.
+
+The Function object is implemented using an L<IO::Async::Routine> with two
+L<IO::Async::Channel> objects to pass calls into and results out from it.
+
+The C<IO::Async> framework generally provides mechanisms for multiplexing IO
+tasks between different handles, so there aren't many occasions when such an
+asynchronous function is necessary. Two cases where this does become useful
+are:
+
+=over 4
+
+=item 1.
+
+When a large amount of computationally-intensive work needs to be performed
+(for example, the C<is_prime> test in the example in the C<SYNOPSIS>).
+
+=item 2.
+
+When a blocking OS syscall or library-level function needs to be called, and
+no nonblocking or asynchronous version is supplied. This is used by
+C<IO::Async::Resolver>.
+
+=back
+
+This object is ideal for representing "pure" functions; that is, blocks of
+code which have no stateful effect on the process, and whose result depends
+only on the arguments passed in. For a more general co-routine ability, see
+also L<IO::Async::Routine>.
+
+=cut
+
+=head1 PARAMETERS
+
+The following named parameters may be passed to C<new> or C<configure>:
+
+=head2 code => CODE
+
+The body of the function to execute.
+
+=head2 model => "fork" | "thread"
+
+Optional. Requests a specific C<IO::Async::Routine> model. If not supplied,
+leaves the default choice up to Routine.
+
+=head2 min_workers => INT
+
+=head2 max_workers => INT
+
+The lower and upper bounds of worker processes to try to keep running. The
+actual number running at any time will be kept somewhere between these bounds
+according to load.
+
+=head2 max_worker_calls => INT
+
+Optional. If provided, stop a worker process after it has processed this
+number of calls. (New workers may be started to replace stopped ones, within
+the bounds given above).
+
+=head2 idle_timeout => NUM
+
+Optional. If provided, idle worker processes will be shut down after this
+amount of time, if there are more than C<min_workers> of them.
+
+=head2 exit_on_die => BOOL
+
+Optional boolean, controls what happens after the C<code> throws an
+exception. If missing or false, the worker will continue running to process
+more requests. If true, the worker will be shut down. A new worker might be
+constructed by the C<call> method to replace it, if necessary.
+
+=head2 setup => ARRAY
+
+Optional array reference. Specifies the C<setup> key to pass to the underlying
+L<IO::Async::Process> when setting up new worker processes.
+
+=cut
+
+sub _init
+{
+ my $self = shift;
+ $self->SUPER::_init( @_ );
+
+ $self->{min_workers} = 1;
+ $self->{max_workers} = 8;
+
+ $self->{workers} = {}; # {$id} => IaFunction:Worker
+
+ $self->{pending_queue} = [];
+}
+
+sub configure
+{
+ my $self = shift;
+ my %params = @_;
+
+ my %worker_params;
+ foreach (qw( model exit_on_die max_worker_calls )) {
+ $self->{$_} = $worker_params{$_} = delete $params{$_} if exists $params{$_};
+ }
+
+ if( keys %worker_params ) {
+ foreach my $worker ( $self->_worker_objects ) {
+ $worker->configure( %worker_params );
+ }
+ }
+
+ if( exists $params{idle_timeout} ) {
+ my $timeout = delete $params{idle_timeout};
+ if( !$timeout ) {
+ $self->remove_child( delete $self->{idle_timer} ) if $self->{idle_timer};
+ }
+ elsif( my $idle_timer = $self->{idle_timer} ) {
+ $idle_timer->configure( delay => $timeout );
+ }
+ else {
+ $self->{idle_timer} = IO::Async::Timer::Countdown->new(
+ delay => $timeout,
+ on_expire => $self->_capture_weakself( sub {
+ my $self = shift or return;
+ my $workers = $self->{workers};
+
+ # Shut down atmost one idle worker, starting from the highest
+ # ID. Since we search from lowest to assign work, this tries
+ # to ensure we'll shut down the least useful ones first,
+ # keeping more useful ones in memory (page/cache warmth, etc..)
+ foreach my $id ( reverse sort keys %$workers ) {
+ next if $workers->{$id}{busy};
+
+ $workers->{$id}->stop;
+ last;
+ }
+
+ # Still more?
+ $self->{idle_timer}->start if $self->workers_idle > $self->{min_workers};
+ } ),
+ );
+ $self->add_child( $self->{idle_timer} );
+ }
+ }
+
+ foreach (qw( min_workers max_workers )) {
+ $self->{$_} = delete $params{$_} if exists $params{$_};
+ # TODO: something about retuning
+ }
+
+ my $need_restart;
+
+ foreach (qw( code setup )) {
+ $need_restart++, $self->{$_} = delete $params{$_} if exists $params{$_};
+ }
+
+ $self->SUPER::configure( %params );
+
+ if( $need_restart and $self->loop ) {
+ $self->stop;
+ $self->start;
+ }
+}
+
+sub _add_to_loop
+{
+ my $self = shift;
+ $self->SUPER::_add_to_loop( @_ );
+
+ $self->start;
+}
+
+sub _remove_from_loop
+{
+ my $self = shift;
+
+ $self->stop;
+
+ $self->SUPER::_remove_from_loop( @_ );
+}
+
+=head1 METHODS
+
+The following methods documented with a trailing call to C<< ->get >> return
+L<Future> instances.
+
+=cut
+
+=head2 $function->start
+
+Start the worker processes
+
+=cut
+
+sub start
+{
+ my $self = shift;
+
+ $self->_new_worker for 1 .. $self->{min_workers};
+}
+
+=head2 $function->stop
+
+Stop the worker processes
+
+=cut
+
+sub stop
+{
+ my $self = shift;
+
+ $self->{stopping} = 1;
+ foreach my $worker ( $self->_worker_objects ) {
+ $worker->stop;
+ }
+}
+
+=head2 $function->restart
+
+Gracefully stop and restart all the worker processes.
+
+=cut
+
+sub restart
+{
+ my $self = shift;
+
+ $self->stop;
+ $self->start;
+}
+
+=head2 @result = $function->call( %params )->get
+
+Schedules an invocation of the contained function to be executed on one of the
+worker processes. If a non-busy worker is available now, it will be called
+immediately. If not, it will be queued and sent to the next free worker that
+becomes available.
+
+The request will already have been serialised by the marshaller, so it will be
+safe to modify any referenced data structures in the arguments after this call
+returns.
+
+The C<%params> hash takes the following keys:
+
+=over 8
+
+=item args => ARRAY
+
+A reference to the array of arguments to pass to the code.
+
+=back
+
+=head2 $function->call( %params )
+
+When not returning a future, the C<on_result>, C<on_return> and C<on_error>
+arguments give continuations to handle successful results or failure.
+
+=over 8
+
+=item on_result => CODE
+
+A continuation that is invoked when the code has been executed. If the code
+returned normally, it is called as:
+
+ $on_result->( 'return', @values )
+
+If the code threw an exception, or some other error occured such as a closed
+connection or the process died, it is called as:
+
+ $on_result->( 'error', $exception_name )
+
+=item on_return => CODE and on_error => CODE
+
+An alternative to C<on_result>. Two continuations to use in either of the
+circumstances given above. They will be called directly, without the leading
+'return' or 'error' value.
+
+=back
+
+=cut
+
+sub call
+{
+ my $self = shift;
+ my %params = @_;
+
+ # TODO: possibly just queue this?
+ $self->loop or croak "Cannot ->call on a Function not yet in a Loop";
+
+ my $args = delete $params{args};
+ ref $args eq "ARRAY" or croak "Expected 'args' to be an array";
+
+ my ( $on_done, $on_fail );
+ if( defined $params{on_result} ) {
+ my $on_result = delete $params{on_result};
+ ref $on_result or croak "Expected 'on_result' to be a reference";
+
+ $on_done = $self->_capture_weakself( sub {
+ my $self = shift or return;
+ $self->debug_printf( "CONT on_result return" );
+ $on_result->( return => @_ );
+ } );
+ $on_fail = $self->_capture_weakself( sub {
+ my $self = shift or return;
+ my ( $err, @values ) = @_;
+ $self->debug_printf( "CONT on_result error" );
+ $on_result->( error => @values );
+ } );
+ }
+ elsif( defined $params{on_return} and defined $params{on_error} ) {
+ my $on_return = delete $params{on_return};
+ ref $on_return or croak "Expected 'on_return' to be a reference";
+ my $on_error = delete $params{on_error};
+ ref $on_error or croak "Expected 'on_error' to be a reference";
+
+ $on_done = $self->_capture_weakself( sub {
+ my $self = shift or return;
+ $self->debug_printf( "CONT on_return" );
+ $on_return->( @_ );
+ } );
+ $on_fail = $self->_capture_weakself( sub {
+ my $self = shift or return;
+ $self->debug_printf( "CONT on_error" );
+ $on_error->( @_ );
+ } );
+ }
+ elsif( !defined wantarray ) {
+ croak "Expected either 'on_result' or 'on_return' and 'on_error' keys, or to return a Future";
+ }
+
+ my $request = freeze( $args );
+
+ my $future;
+ if( my $worker = $self->_get_worker ) {
+ $self->debug_printf( "CALL" );
+ $future = $self->_call_worker( $worker, $request );
+ }
+ else {
+ $self->debug_printf( "QUEUE" );
+ push @{ $self->{pending_queue} }, my $wait_f = $self->loop->new_future;
+
+ $future = $wait_f->then( sub {
+ my ( $self, $worker ) = @_;
+ $self->_call_worker( $worker, $request );
+ });
+ }
+
+ $future->on_done( $on_done ) if $on_done;
+ $future->on_fail( $on_fail ) if $on_fail;
+
+ return $future if defined wantarray;
+
+ # Caller is not going to keep hold of the Future, so we have to ensure it
+ # stays alive somehow
+ $self->adopt_future( $future->else( sub { Future->done } ) );
+}
+
+sub _worker_objects
+{
+ my $self = shift;
+ return values %{ $self->{workers} };
+}
+
+=head2 $count = $function->workers
+
+Returns the total number of worker processes available
+
+=cut
+
+sub workers
+{
+ my $self = shift;
+ return scalar keys %{ $self->{workers} };
+}
+
+=head2 $count = $function->workers_busy
+
+Returns the number of worker processes that are currently busy
+
+=cut
+
+sub workers_busy
+{
+ my $self = shift;
+ return scalar grep { $_->{busy} } $self->_worker_objects;
+}
+
+=head2 $count = $function->workers_idle
+
+Returns the number of worker processes that are currently idle
+
+=cut
+
+sub workers_idle
+{
+ my $self = shift;
+ return scalar grep { !$_->{busy} } $self->_worker_objects;
+}
+
+sub _new_worker
+{
+ my $self = shift;
+
+ my $worker = IO::Async::Function::Worker->new(
+ ( map { $_ => $self->{$_} } qw( model code setup exit_on_die ) ),
+ max_calls => $self->{max_worker_calls},
+
+ on_finish => $self->_capture_weakself( sub {
+ my $self = shift or return;
+ my ( $worker ) = @_;
+
+ return if $self->{stopping};
+
+ $self->_new_worker if $self->workers < $self->{min_workers};
+
+ $self->_dispatch_pending;
+ } ),
+ );
+
+ $self->add_child( $worker );
+
+ return $self->{workers}{$worker->id} = $worker;
+}
+
+sub _get_worker
+{
+ my $self = shift;
+
+ foreach ( sort keys %{ $self->{workers} } ) {
+ return $self->{workers}{$_} if !$self->{workers}{$_}{busy};
+ }
+
+ if( $self->workers < $self->{max_workers} ) {
+ return $self->_new_worker;
+ }
+
+ return undef;
+}
+
+sub _call_worker
+{
+ my $self = shift;
+ my ( $worker, $type, $args ) = @_;
+
+ my $future = $worker->call( $type, $args );
+
+ if( $self->workers_idle == 0 ) {
+ $self->{idle_timer}->stop if $self->{idle_timer};
+ }
+
+ return $future;
+}
+
+sub _dispatch_pending
+{
+ my $self = shift;
+
+ while( my $next = shift @{ $self->{pending_queue} } ) {
+ my $worker = $self->_get_worker or return;
+
+ next if $next->is_cancelled;
+
+ $self->debug_printf( "UNQUEUE" );
+ $next->done( $self, $worker );
+ return;
+ }
+
+ if( $self->workers_idle > $self->{min_workers} ) {
+ $self->{idle_timer}->start if $self->{idle_timer} and !$self->{idle_timer}->is_running;
+ }
+}
+
+package # hide from indexer
+ IO::Async::Function::Worker;
+
+use base qw( IO::Async::Routine );
+
+use IO::Async::Channel;
+
+sub new
+{
+ my $class = shift;
+ my %params = @_;
+
+ my $arg_channel = IO::Async::Channel->new;
+ my $ret_channel = IO::Async::Channel->new;
+
+ my $code = delete $params{code};
+ $params{code} = sub {
+ while( my $args = $arg_channel->recv ) {
+ my @ret;
+ my $ok = eval { @ret = $code->( @$args ); 1 };
+
+ if( $ok ) {
+ $ret_channel->send( [ r => @ret ] );
+ }
+ else {
+ chomp( my $e = "$@" );
+ $ret_channel->send( [ e => $e, error => ] );
+ }
+ }
+ };
+
+ my $worker = $class->SUPER::new(
+ %params,
+ channels_in => [ $arg_channel ],
+ channels_out => [ $ret_channel ],
+ );
+
+ $worker->{arg_channel} = $arg_channel;
+ $worker->{ret_channel} = $ret_channel;
+
+ return $worker;
+}
+
+sub configure
+{
+ my $self = shift;
+ my %params = @_;
+
+ exists $params{$_} and $self->{$_} = delete $params{$_} for qw( exit_on_die max_calls );
+
+ $self->SUPER::configure( %params );
+}
+
+sub stop
+{
+ my $worker = shift;
+ $worker->{arg_channel}->close;
+
+ if( my $function = $worker->parent ) {
+ delete $function->{workers}{$worker->id};
+
+ if( $worker->{busy} ) {
+ $worker->{remove_on_idle}++;
+ }
+ else {
+ $function->remove_child( $worker );
+ }
+ }
+}
+
+sub call
+{
+ my $worker = shift;
+ my ( $args ) = @_;
+
+ $worker->{arg_channel}->send_frozen( $args );
+
+ $worker->{busy} = 1;
+ $worker->{max_calls}--;
+
+ return $worker->{ret_channel}->recv->then(
+ # on recv
+ $worker->_capture_weakself( sub {
+ my ( $worker, $result ) = @_;
+ my ( $type, @values ) = @$result;
+
+ $worker->stop if !$worker->{max_calls} or
+ $worker->{exit_on_die} && $type eq "e";
+
+ if( $type eq "r" ) {
+ return Future->done( @values );
+ }
+ elsif( $type eq "e" ) {
+ return Future->fail( @values );
+ }
+ else {
+ die "Unrecognised type from worker - $type\n";
+ }
+ } ),
+ # on EOF
+ $worker->_capture_weakself( sub {
+ my ( $worker ) = @_;
+
+ $worker->stop;
+
+ return Future->fail( "closed", "closed" );
+ } )
+ )->on_ready( $worker->_capture_weakself( sub {
+ my ( $worker, $f ) = @_;
+ $worker->{busy} = 0;
+
+ my $function = $worker->parent;
+ $function->_dispatch_pending if $function;
+
+ $function->remove_child( $worker ) if $function and $worker->{remove_on_idle};
+ }));
+}
+
+=head1 NOTES
+
+For the record, 123454321 is 11111 * 11111, a square number, and therefore not
+prime.
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/Future.pm b/lib/IO/Async/Future.pm
new file mode 100644
index 0000000..5bf8395
--- /dev/null
+++ b/lib/IO/Async/Future.pm
@@ -0,0 +1,150 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2013 -- leonerd@leonerd.org.uk
+
+package IO::Async::Future;
+
+use strict;
+use warnings;
+
+our $VERSION = '0.67';
+
+use base qw( Future );
+Future->VERSION( '0.05' ); # to respect subclassing
+
+use Carp;
+
+=head1 NAME
+
+C<IO::Async::Future> - use L<Future> with L<IO::Async>
+
+=head1 SYNOPSIS
+
+ use IO::Async::Loop;
+
+ my $loop = IO::Async::Loop->new;
+
+ my $future = $loop->new_future;
+
+ $loop->watch_time( after => 3, code => sub { $future->done( "Done" ) } );
+
+ print $future->get, "\n";
+
+=head1 DESCRIPTION
+
+This subclass of L<Future> stores a reference to the L<IO::Async::Loop>
+instance that created it, allowing the C<await> method to block until the
+Future is ready. These objects should not be constructed directly; instead
+the C<new_future> method on the containing Loop should be used.
+
+For a full description on how to use Futures, see the L<Future> documentation.
+
+=cut
+
+=head1 CONSTRUCTORS
+
+New C<IO::Async::Future> objects should be constructed by using the following
+methods on the C<Loop>. For more detail see the L<IO::Async::Loop>
+documentation.
+
+=head2 $future = $loop->new_future
+
+Returns a new pending Future.
+
+=head2 $future = $loop->delay_future( %args )
+
+Returns a new Future that will become done at a given time.
+
+=head2 $future = $loop->timeout_future( %args )
+
+Returns a new Future that will become failed at a given time.
+
+=cut
+
+sub new
+{
+ my $proto = shift;
+ my $self = $proto->SUPER::new;
+
+ if( ref $proto ) {
+ $self->{loop} = $proto->{loop};
+ }
+ else {
+ $self->{loop} = shift;
+ }
+
+ return $self;
+}
+
+=head1 METHODS
+
+=cut
+
+=head2 $loop = $future->loop
+
+Returns the underlying C<IO::Async::Loop> object.
+
+=cut
+
+sub loop
+{
+ my $self = shift;
+ return $self->{loop};
+}
+
+sub await
+{
+ my $self = shift;
+ $self->{loop}->loop_once;
+}
+
+=head2 $future->done_later( @result )
+
+A shortcut to calling the C<done> method in a C<later> idle watch on the
+underlying Loop object. Ensures that a returned Future object is not ready
+immediately, but will wait for the next IO round.
+
+Like C<done>, returns C<$future> itself to allow easy chaining.
+
+=cut
+
+sub done_later
+{
+ my $self = shift;
+ my @result = @_;
+
+ $self->loop->later( sub { $self->done( @result ) } );
+
+ return $self;
+}
+
+=head2 $future->fail_later( $exception, @details )
+
+A shortcut to calling the C<fail> method in a C<later> idle watch on the
+underlying Loop object. Ensures that a returned Future object is not ready
+immediately, but will wait for the next IO round.
+
+Like C<fail>, returns C<$future> itself to allow easy chaining.
+
+=cut
+
+sub fail_later
+{
+ my $self = shift;
+ my ( $exception, @details ) = @_;
+
+ $exception or croak "Expected a true exception";
+
+ $self->loop->later( sub { $self->fail( $exception, @details ) } );
+
+ return $self;
+}
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/Handle.pm b/lib/IO/Async/Handle.pm
new file mode 100644
index 0000000..201d900
--- /dev/null
+++ b/lib/IO/Async/Handle.pm
@@ -0,0 +1,687 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2006-2015 -- leonerd@leonerd.org.uk
+
+package IO::Async::Handle;
+
+use strict;
+use warnings;
+use base qw( IO::Async::Notifier );
+
+our $VERSION = '0.67';
+
+use Carp;
+
+use IO::Handle; # give methods to bare IO handles
+
+use Future;
+use Future::Utils qw( try_repeat );
+
+use IO::Async::OS;
+
+=head1 NAME
+
+C<IO::Async::Handle> - event callbacks for a non-blocking file descriptor
+
+=head1 SYNOPSIS
+
+This class is likely not to be used directly, because subclasses of it exist
+to handle more specific cases. Here is an example of how it would be used to
+watch a listening socket for new connections. In real code, it is likely that
+the C<< Loop->listen >> method would be used instead.
+
+ use IO::Socket::INET;
+ use IO::Async::Handle;
+
+ use IO::Async::Loop;
+ my $loop = IO::Async::Loop->new;
+
+ my $socket = IO::Socket::INET->new( LocalPort => 1234, Listen => 1 );
+
+ my $handle = IO::Async::Handle->new(
+ handle => $socket,
+
+ on_read_ready => sub {
+ my $new_client = $socket->accept;
+ ...
+ },
+ );
+
+ $loop->add( $handle );
+
+For most other uses with sockets, pipes or other filehandles that carry a byte
+stream, the L<IO::Async::Stream> class is likely to be more suitable. For
+non-stream sockets, see L<IO::Async::Socket>.
+
+=head1 DESCRIPTION
+
+This subclass of L<IO::Async::Notifier> allows non-blocking IO on filehandles.
+It provides event handlers for when the filehandle is read- or write-ready.
+
+=cut
+
+=head1 EVENTS
+
+The following events are invoked, either using subclass methods or CODE
+references in parameters:
+
+=head2 on_read_ready
+
+Invoked when the read handle becomes ready for reading.
+
+=head2 on_write_ready
+
+Invoked when the write handle becomes ready for writing.
+
+=head2 on_closed
+
+Optional. Invoked when the handle becomes closed.
+
+This handler is invoked before the filehandles are closed and the Handle
+removed from its containing Loop. The C<loop> will still return the containing
+Loop object.
+
+=cut
+
+=head1 PARAMETERS
+
+The following named parameters may be passed to C<new> or C<configure>:
+
+=head2 read_handle => IO
+
+=head2 write_handle => IO
+
+The reading and writing IO handles. Each must implement the C<fileno> method.
+Primarily used for passing C<STDIN> / C<STDOUT>; see the SYNOPSIS section of
+C<IO::Async::Stream> for an example.
+
+=head2 handle => IO
+
+The IO handle for both reading and writing; instead of passing each separately
+as above. Must implement C<fileno> method in way that C<IO::Handle> does.
+
+=head2 read_fileno => INT
+
+=head2 write_fileno => INT
+
+File descriptor numbers for reading and writing. If these are given as an
+alternative to C<read_handle> or C<write_handle> then a new C<IO::Handle>
+instance will be constructed around each.
+
+=head2 on_read_ready => CODE
+
+=head2 on_write_ready => CODE
+
+=head2 on_closed => CODE
+
+CODE references for event handlers.
+
+=head2 want_readready => BOOL
+
+=head2 want_writeready => BOOL
+
+If present, enable or disable read- or write-ready notification as per the
+C<want_readready> and C<want_writeready> methods.
+
+It is required that a matching C<on_read_ready> or C<on_write_ready> are
+available for any handle that is provided; either passed as a callback CODE
+reference or as an overridden the method. I.e. if only a C<read_handle> is
+given, then C<on_write_ready> can be absent. If C<handle> is used as a
+shortcut, then both read and write-ready callbacks or methods are required.
+
+If no IO handles are provided at construction time, the object is still
+created but will not yet be fully-functional as a Handle. IO handles can be
+assigned later using the C<set_handle> or C<set_handles> methods, or by
+C<configure>. This may be useful when constructing an object to represent a
+network connection, before the C<connect(2)> has actually been performed yet.
+
+=cut
+
+sub configure
+{
+ my $self = shift;
+ my %params = @_;
+
+ if( exists $params{on_read_ready} ) {
+ $self->{on_read_ready} = delete $params{on_read_ready};
+ undef $self->{cb_r};
+
+ $self->_watch_read(0), $self->_watch_read(1) if $self->want_readready;
+ }
+
+ if( exists $params{on_write_ready} ) {
+ $self->{on_write_ready} = delete $params{on_write_ready};
+ undef $self->{cb_w};
+
+ $self->_watch_write(0), $self->_watch_write(1) if $self->want_writeready;
+ }
+
+ if( exists $params{on_closed} ) {
+ $self->{on_closed} = delete $params{on_closed};
+ }
+
+ if( defined $params{read_fileno} and defined $params{write_fileno} and
+ $params{read_fileno} == $params{write_fileno} ) {
+ $params{handle} = IO::Handle->new_from_fd( $params{read_fileno}, "r+" );
+
+ delete $params{read_fileno};
+ delete $params{write_fileno};
+ }
+ else {
+ $params{read_handle} = IO::Handle->new_from_fd( delete $params{read_fileno}, "r" )
+ if defined $params{read_fileno};
+
+ $params{write_handle} = IO::Handle->new_from_fd( delete $params{write_fileno}, "w" )
+ if defined $params{write_fileno};
+ }
+
+ # 'handle' is a shortcut for setting read_ and write_
+ if( exists $params{handle} ) {
+ $params{read_handle} = $params{handle};
+ $params{write_handle} = $params{handle};
+ delete $params{handle};
+ }
+
+ if( exists $params{read_handle} ) {
+ my $read_handle = delete $params{read_handle};
+
+ if( defined $read_handle ) {
+ if( !defined eval { $read_handle->fileno } ) {
+ croak 'Expected that read_handle can ->fileno';
+ }
+
+ unless( $self->can_event( 'on_read_ready' ) ) {
+ croak 'Expected either a on_read_ready callback or an ->on_read_ready method';
+ }
+
+ my @layers = PerlIO::get_layers( $read_handle );
+ if( grep m/^encoding\(/, @layers or grep m/^utf8$/, @layers ) {
+ # Only warn for now, because if it's UTF-8 by default but only
+ # passes ASCII then all will be well
+ carp "Constructing a ".ref($self)." with an encoding-enabled handle may not read correctly";
+ }
+ }
+
+ $self->{read_handle} = $read_handle;
+
+ $self->want_readready( defined $read_handle );
+
+ # In case someone has reopened the filehandles during an on_closed handler
+ undef $self->{handle_closing};
+ }
+
+ if( exists $params{write_handle} ) {
+ my $write_handle = delete $params{write_handle};
+
+ if( defined $write_handle ) {
+ if( !defined eval { $write_handle->fileno } ) {
+ croak 'Expected that write_handle can ->fileno';
+ }
+
+ unless( $self->can_event( 'on_write_ready' ) ) {
+ # This used not to be fatal. Make it just a warning for now.
+ carp 'A write handle was provided but neither a on_write_ready callback nor an ->on_write_ready method were. Perhaps you mean \'read_handle\' instead?';
+ }
+ }
+
+ $self->{write_handle} = $write_handle;
+
+ # In case someone has reopened the filehandles during an on_closed handler
+ undef $self->{handle_closing};
+ }
+
+ if( exists $params{want_readready} ) {
+ $self->want_readready( delete $params{want_readready} );
+ }
+
+ if( exists $params{want_writeready} ) {
+ $self->want_writeready( delete $params{want_writeready} );
+ }
+
+ $self->SUPER::configure( %params );
+}
+
+# We'll be calling these any of three times
+# adding to/removing from loop
+# caller en/disables readiness checking
+# changing filehandle
+
+sub _watch_read
+{
+ my $self = shift;
+ my ( $want ) = @_;
+
+ my $loop = $self->loop or return;
+ my $fh = $self->read_handle or return;
+
+ if( $want ) {
+ $self->{cb_r} ||= $self->make_event_cb( 'on_read_ready' );
+
+ $loop->watch_io(
+ handle => $fh,
+ on_read_ready => $self->{cb_r},
+ );
+ }
+ else {
+ $loop->unwatch_io(
+ handle => $fh,
+ on_read_ready => 1,
+ );
+ }
+}
+
+sub _watch_write
+{
+ my $self = shift;
+ my ( $want ) = @_;
+
+ my $loop = $self->loop or return;
+ my $fh = $self->write_handle or return;
+
+ if( $want ) {
+ $self->{cb_w} ||= $self->make_event_cb( 'on_write_ready' );
+
+ $loop->watch_io(
+ handle => $fh,
+ on_write_ready => $self->{cb_w},
+ );
+ }
+ else {
+ $loop->unwatch_io(
+ handle => $fh,
+ on_write_ready => 1,
+ );
+ }
+}
+
+sub _add_to_loop
+{
+ my $self = shift;
+ my ( $loop ) = @_;
+
+ $self->_watch_read(1) if $self->want_readready;
+ $self->_watch_write(1) if $self->want_writeready;
+}
+
+sub _remove_from_loop
+{
+ my $self = shift;
+ my ( $loop ) = @_;
+
+ $self->_watch_read(0);
+ $self->_watch_write(0);
+}
+
+sub notifier_name
+{
+ my $self = shift;
+ if( length( my $name = $self->SUPER::notifier_name ) ) {
+ return $name;
+ }
+
+ my $r = $self->read_fileno;
+ my $w = $self->write_fileno;
+ return "rw=$r" if defined $r and defined $w and $r == $w;
+ return "r=$r,w=$w" if defined $r and defined $w;
+ return "r=$r" if defined $r;
+ return "w=$w" if defined $w;
+ return "no";
+}
+
+=head1 METHODS
+
+The following methods documented with a trailing call to C<< ->get >> return
+L<Future> instances.
+
+=cut
+
+=head2 $handle->set_handles( %params )
+
+Sets new reading or writing filehandles. Equivalent to calling the
+C<configure> method with the same parameters.
+
+=cut
+
+sub set_handles
+{
+ my $self = shift;
+ my %params = @_;
+
+ $self->configure(
+ exists $params{read_handle} ? ( read_handle => $params{read_handle} ) : (),
+ exists $params{write_handle} ? ( write_handle => $params{write_handle} ) : (),
+ );
+}
+
+=head2 $handle->set_handle( $fh )
+
+Shortcut for
+
+ $handle->configure( handle => $fh )
+
+=cut
+
+sub set_handle
+{
+ my $self = shift;
+ my ( $fh ) = @_;
+
+ $self->configure( handle => $fh );
+}
+
+=head2 $handle->close
+
+This method calls C<close> on the underlying IO handles. This method will then
+remove the handle from its containing loop.
+
+=cut
+
+sub close
+{
+ my $self = shift;
+
+ # Prevent infinite loops if there's two crosslinked handles
+ return if $self->{handle_closing};
+ $self->{handle_closing} = 1;
+
+ $self->want_readready( 0 );
+ $self->want_writeready( 0 );
+
+ my $read_handle = delete $self->{read_handle};
+ $read_handle->close if defined $read_handle;
+
+ my $write_handle = delete $self->{write_handle};
+ $write_handle->close if defined $write_handle;
+
+ $self->_closed;
+}
+
+sub _closed
+{
+ my $self = shift;
+
+ $self->maybe_invoke_event( on_closed => );
+ if( $self->{close_futures} ) {
+ $_->done for @{ $self->{close_futures} };
+ }
+ $self->remove_from_parent;
+}
+
+=head2 $handle->close_read
+
+=head2 $handle->close_write
+
+Closes the underlying read or write handle, and deconfigures it from the
+object. Neither of these methods will invoke the C<on_closed> event, nor
+remove the object from the Loop if there is still one open handle in the
+object. Only when both handles are closed, will C<on_closed> be fired, and the
+object removed.
+
+=cut
+
+sub close_read
+{
+ my $self = shift;
+
+ $self->want_readready( 0 );
+
+ my $read_handle = delete $self->{read_handle};
+ $read_handle->close if defined $read_handle;
+
+ $self->_closed if !$self->{write_handle};
+}
+
+sub close_write
+{
+ my $self = shift;
+
+ $self->want_writeready( 0 );
+
+ my $write_handle = delete $self->{write_handle};
+ $write_handle->close if defined $write_handle;
+
+ $self->_closed if !$self->{read_handle};
+}
+
+=head2 $handle->new_close_future->get
+
+Returns a new L<IO::Async::Future> object which will become done when the
+handle is closed. Cancelling the C<$future> will remove this notification
+ability but will not otherwise affect the C<$handle>.
+
+=cut
+
+sub new_close_future
+{
+ my $self = shift;
+
+ push @{ $self->{close_futures} }, my $future = $self->loop->new_future;
+ $future->on_cancel(
+ $self->_capture_weakself( sub {
+ my $self = shift or return;
+ my $future = shift;
+
+ @{ $self->{close_futures} } = grep { $_ != $future } @{ $self->{close_futures} };
+ })
+ );
+
+ return $future;
+}
+
+=head2 $handle = $handle->read_handle
+
+=head2 $handle = $handle->write_handle
+
+These accessors return the underlying IO handles.
+
+=cut
+
+sub read_handle
+{
+ my $self = shift;
+ return $self->{read_handle};
+}
+
+sub write_handle
+{
+ my $self = shift;
+ return $self->{write_handle};
+}
+
+=head2 $fileno = $handle->read_fileno
+
+=head2 $fileno = $handle->write_fileno
+
+These accessors return the file descriptor numbers of the underlying IO
+handles.
+
+=cut
+
+sub read_fileno
+{
+ my $self = shift;
+ my $handle = $self->read_handle or return undef;
+ return $handle->fileno;
+}
+
+sub write_fileno
+{
+ my $self = shift;
+ my $handle = $self->write_handle or return undef;
+ return $handle->fileno;
+}
+
+=head2 $value = $handle->want_readready
+
+=head2 $oldvalue = $handle->want_readready( $newvalue )
+
+=head2 $value = $handle->want_writeready
+
+=head2 $oldvalue = $handle->want_writeready( $newvalue )
+
+These are the accessor for the C<want_readready> and C<want_writeready>
+properties, which define whether the object is interested in knowing about
+read- or write-readiness on the underlying file handle.
+
+=cut
+
+sub want_readready
+{
+ my $self = shift;
+ if( @_ ) {
+ my ( $new ) = @_;
+
+ $new = !!$new;
+ return $new if !$new == !$self->{want_readready}; # compare bools
+
+ if( $new ) {
+ defined $self->read_handle or
+ croak 'Cannot want_readready in a Handle with no read_handle';
+ }
+
+ my $old = $self->{want_readready};
+ $self->{want_readready} = $new;
+
+ $self->_watch_read( $new );
+
+ return $old;
+ }
+ else {
+ return $self->{want_readready};
+ }
+}
+
+sub want_writeready
+{
+ my $self = shift;
+ if( @_ ) {
+ my ( $new ) = @_;
+
+ $new = !!$new;
+ return $new if !$new == !$self->{want_writeready}; # compare bools
+
+ if( $new ) {
+ defined $self->write_handle or
+ croak 'Cannot want_writeready in a Handle with no write_handle';
+ }
+
+ my $old = $self->{want_writeready};
+ $self->{want_writeready} = $new;
+
+ $self->_watch_write( $new );
+
+ return $old;
+ }
+ else {
+ return $self->{want_writeready};
+ }
+}
+
+=head2 $handle->socket( $ai )
+
+Convenient shortcut to creating a socket handle, as given by an addrinfo
+structure, and setting it as the read and write handle for the object.
+
+C<$ai> may be either a C<HASH> or C<ARRAY> reference of the same form as given
+to L<IO::Async::OS>'s C<extract_addrinfo> method.
+
+This method returns nothing if it succeeds, or throws an exception if it
+fails.
+
+=cut
+
+sub socket
+{
+ my $self = shift;
+ my ( $ai ) = @_;
+
+ # TODO: Something about closing the old one?
+
+ my ( $family, $socktype, $protocol ) = IO::Async::OS->extract_addrinfo( $ai );
+
+ my $sock = IO::Async::OS->socket( $family, $socktype, $protocol );
+ $self->set_handle( $sock );
+}
+
+=head2 $handle = $handle->bind( %args )->get
+
+Performs a C<getaddrinfo> resolver operation with the C<passive> flag set,
+and then attempts to bind a socket handle of any of the return values.
+
+=head2 $handle = $handle->bind( $ai )->get
+
+When invoked with a single argument, this method is a convenient shortcut to
+creating a socket handle and C<bind()>ing it to the address as given by an
+addrinfo structure, and setting it as the read and write handle for the
+object.
+
+C<$ai> may be either a C<HASH> or C<ARRAY> reference of the same form as given
+to L<IO::Async::OS>'s C<extract_addrinfo> method.
+
+The returned future returns the handle object itself for convenience.
+
+=cut
+
+sub bind
+{
+ my $self = shift;
+
+ if( @_ == 1 ) {
+ my ( $ai ) = @_;
+
+ $self->socket( $ai );
+ my $addr = ( IO::Async::OS->extract_addrinfo( $ai ) )[3];
+
+ $self->read_handle->bind( $addr ) or
+ return Future->fail( "Cannot bind - $!", bind => $self->read_handle, $addr, $! );
+
+ return Future->done( $self );
+ }
+
+ $self->loop->resolver->getaddrinfo( passive => 1, @_ )->then( sub {
+ my @addrs = @_;
+
+ try_repeat {
+ my $ai = shift;
+
+ $self->bind( $ai );
+ } foreach => \@addrs,
+ until => sub { shift->is_done };
+ });
+}
+
+=head2 $handle = $handle->connect( %args )->get
+
+A convenient wrapper for calling the C<connect> method on the underlying
+L<IO::Async::Loop> object.
+
+=cut
+
+sub connect
+{
+ my $self = shift;
+ my %args = @_;
+
+ my $loop = $self->loop or croak "Cannot ->connect a Handle that is not in a Loop";
+
+ return $self->loop->connect( %args, handle => $self );
+}
+
+=head1 SEE ALSO
+
+=over 4
+
+=item *
+
+L<IO::Handle> - Supply object methods for I/O handles
+
+=back
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/Internals/Connector.pm b/lib/IO/Async/Internals/Connector.pm
new file mode 100644
index 0000000..57029ef
--- /dev/null
+++ b/lib/IO/Async/Internals/Connector.pm
@@ -0,0 +1,243 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2008-2013 -- leonerd@leonerd.org.uk
+
+package # hide from CPAN
+ IO::Async::Internals::Connector;
+
+use strict;
+use warnings;
+
+our $VERSION = '0.67';
+
+use Scalar::Util qw( weaken );
+
+use POSIX qw( EINPROGRESS );
+use Socket qw( SOL_SOCKET SO_ERROR );
+
+use Future;
+use Future::Utils 0.18 qw( try_repeat_until_success );
+
+use IO::Async::OS;
+
+use Carp;
+
+use constant CONNECT_EWOULDLBOCK => IO::Async::OS->HAVE_CONNECT_EWOULDBLOCK;
+
+# Internal constructor
+sub new
+{
+ my $class = shift;
+ my ( %params ) = @_;
+
+ my $loop = delete $params{loop} or croak "Expected a 'loop'";
+
+ my $self = bless {}, $class;
+ weaken( $self->{loop} = $loop );
+
+ return $self;
+}
+
+## Utility function
+sub _get_sock_err
+{
+ my ( $sock ) = @_;
+
+ my $err = $sock->getsockopt( SOL_SOCKET, SO_ERROR );
+
+ if( defined $err ) {
+ # 0 means no error, but is still defined
+ return undef if !$err;
+
+ $! = $err;
+ return $!;
+ }
+
+ # It seems we can't call getsockopt to query SO_ERROR. We'll try getpeername
+ if( defined getpeername( $sock ) ) {
+ return undef;
+ }
+
+ my $peername_errno = $!+0;
+ my $peername_errstr = "$!";
+
+ # Not connected so we know this ought to fail
+ if( read( $sock, my $buff, 1 ) ) {
+ # That was most unexpected. getpeername fails because we're not
+ # connected, yet read succeeds.
+ warn "getpeername fails with $peername_errno ($peername_errstr) but read is successful\n";
+ warn "Please see http://rt.cpan.org/Ticket/Display.html?id=38382\n";
+
+ $! = $peername_errno;
+ return $!;
+ }
+
+ return $!;
+}
+
+sub _connect_addresses
+{
+ my $self = shift;
+ my ( $addrlist, $on_fail ) = @_;
+
+ my $loop = $self->{loop};
+
+ my ( $connecterr, $binderr, $socketerr );
+
+ my $future = try_repeat_until_success {
+ my $addr = shift;
+ my ( $family, $socktype, $protocol, $localaddr, $peeraddr ) =
+ @{$addr}{qw( family socktype protocol localaddr peeraddr )};
+
+ my $sock = IO::Async::OS->socket( $family, $socktype, $protocol );
+
+ if( !$sock ) {
+ $socketerr = $!;
+ $on_fail->( "socket", $family, $socktype, $protocol, $! ) if $on_fail;
+ return Future->fail( 1 );
+ }
+
+ if( $localaddr and not $sock->bind( $localaddr ) ) {
+ $binderr = $!;
+ $on_fail->( "bind", $sock, $localaddr, $! ) if $on_fail;
+ return Future->fail( 1 );
+ }
+
+ $sock->blocking( 0 );
+
+ # TODO: $sock->connect returns success masking EINPROGRESS
+ my $ret = connect( $sock, $peeraddr );
+ if( $ret ) {
+ # Succeeded already? Dubious, but OK. Can happen e.g. with connections to
+ # localhost, or UNIX sockets, or something like that.
+ return Future->done( $sock );
+ }
+ elsif( $! != EINPROGRESS and !CONNECT_EWOULDLBOCK || $! != POSIX::EWOULDBLOCK ) {
+ $connecterr = $!;
+ $on_fail->( "connect", $sock, $peeraddr, $! ) if $on_fail;
+ return Future->fail( 1 );
+ }
+
+ # Else
+ my $f = $loop->new_future;
+ $loop->watch_io(
+ handle => $sock,
+ on_write_ready => sub {
+ $loop->unwatch_io( handle => $sock, on_write_ready => 1 );
+
+ my $err = _get_sock_err( $sock );
+
+ return $f->done( $sock ) if !$err;
+
+ $connecterr = $!;
+ $on_fail->( "connect", $sock, $peeraddr, $err ) if $on_fail;
+ return $f->fail( 1 );
+ },
+ );
+ $f->on_cancel(
+ sub { $loop->unwatch_io( handle => $sock, on_write_ready => 1 ); }
+ );
+ return $f;
+ } foreach => $addrlist;
+
+ return $future->else( sub {
+ return $future->new->fail( "connect: $connecterr", connect => connect => $connecterr )
+ if $connecterr;
+ return $future->new->fail( "bind: $binderr", connect => bind => $binderr )
+ if $binderr;
+ return $future->new->fail( "socket: $socketerr", connect => socket => $socketerr )
+ if $socketerr;
+ # If it gets this far then something went wrong
+ die 'Oops; $loop->connect failed but no error cause was found';
+ } );
+}
+
+sub connect
+{
+ my $self = shift;
+ my ( %params ) = @_;
+
+ my $loop = $self->{loop};
+
+ my $on_fail = $params{on_fail};
+
+ my %gai_hints;
+ exists $params{$_} and $gai_hints{$_} = $params{$_} for qw( family socktype protocol flags );
+
+ if( exists $params{host} or exists $params{local_host} or exists $params{local_port} ) {
+ # We'll be making a ->getaddrinfo call
+ defined $gai_hints{socktype} or defined $gai_hints{protocol} or
+ carp "Attempting to ->connect without either 'socktype' or 'protocol' hint is not portable";
+ }
+
+ my $peeraddrfuture;
+ if( exists $params{host} and exists $params{service} ) {
+ my $host = $params{host} or croak "Expected 'host'";
+ my $service = $params{service} or croak "Expected 'service'";
+
+ $peeraddrfuture = $loop->resolver->getaddrinfo(
+ host => $host,
+ service => $service,
+ %gai_hints,
+ );
+ }
+ elsif( exists $params{addrs} or exists $params{addr} ) {
+ $peeraddrfuture = $loop->new_future->done( exists $params{addrs} ? @{ $params{addrs} } : ( $params{addr} ) );
+ }
+ else {
+ croak "Expected 'host' and 'service' or 'addrs' or 'addr' arguments";
+ }
+
+ my $localaddrfuture;
+ if( defined $params{local_host} or defined $params{local_service} ) {
+ # Empty is fine on either of these
+ my $host = $params{local_host};
+ my $service = $params{local_service};
+
+ $localaddrfuture = $loop->resolver->getaddrinfo(
+ host => $host,
+ service => $service,
+ %gai_hints,
+ );
+ }
+ elsif( exists $params{local_addrs} or exists $params{local_addr} ) {
+ $localaddrfuture = $loop->new_future->done( exists $params{local_addrs} ? @{ $params{local_addrs} } : ( $params{local_addr} ) );
+ }
+ else {
+ $localaddrfuture = $loop->new_future->done( {} );
+ }
+
+ return Future->needs_all( $peeraddrfuture, $localaddrfuture )
+ ->then( sub {
+ my @peeraddrs = $peeraddrfuture->get;
+ my @localaddrs = $localaddrfuture->get;
+
+ my @addrs;
+
+ foreach my $local ( @localaddrs ) {
+ my ( $l_family, $l_socktype, $l_protocol, $l_addr ) =
+ IO::Async::OS->extract_addrinfo( $local, 'local_addr' );
+ foreach my $peer ( @peeraddrs ) {
+ my ( $p_family, $p_socktype, $p_protocol, $p_addr ) =
+ IO::Async::OS->extract_addrinfo( $peer );
+
+ next if $l_family and $p_family and $l_family != $p_family;
+ next if $l_socktype and $p_socktype and $l_socktype != $p_socktype;
+ next if $l_protocol and $p_protocol and $l_protocol != $p_protocol;
+
+ push @addrs, {
+ family => $l_family || $p_family,
+ socktype => $l_socktype || $p_socktype,
+ protocol => $l_protocol || $p_protocol,
+ localaddr => $l_addr,
+ peeraddr => $p_addr,
+ };
+ }
+ }
+
+ return $self->_connect_addresses( \@addrs, $on_fail );
+ } );
+}
+
+0x55AA;
diff --git a/lib/IO/Async/Internals/TimeQueue.pm b/lib/IO/Async/Internals/TimeQueue.pm
new file mode 100644
index 0000000..4278fbd
--- /dev/null
+++ b/lib/IO/Async/Internals/TimeQueue.pm
@@ -0,0 +1,205 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2006-2012 -- leonerd@leonerd.org.uk
+
+package # hide from CPAN
+ IO::Async::Internals::TimeQueue;
+
+use strict;
+use warnings;
+
+use Carp;
+
+use Time::HiRes qw( time );
+
+BEGIN {
+ my @methods = qw( next_time _enqueue cancel _fire );
+ if( eval { require Heap::Fibonacci } ) {
+ unshift our @ISA, "Heap::Fibonacci";
+ require Heap::Elem;
+ no strict 'refs';
+ *$_ = \&{"HEAP_$_"} for @methods;
+ }
+ else {
+ no strict 'refs';
+ *$_ = \&{"ARRAY_$_"} for "new", @methods;
+ }
+}
+
+# High-level methods
+
+sub enqueue
+{
+ my $self = shift;
+ my ( %params ) = @_;
+
+ my $code = delete $params{code};
+ ref $code or croak "Expected 'code' to be a reference";
+
+ defined $params{time} or croak "Expected 'time'";
+ my $time = $params{time};
+
+ $self->_enqueue( $time, $code );
+}
+
+sub fire
+{
+ my $self = shift;
+ my ( %params ) = @_;
+
+ my $now = exists $params{now} ? $params{now} : time;
+ $self->_fire( $now );
+}
+
+# Implementation using a Perl array
+
+use constant {
+ TIME => 0,
+ CODE => 1,
+};
+
+sub ARRAY_new
+{
+ my $class = shift;
+ return bless [], $class;
+}
+
+sub ARRAY_next_time
+{
+ my $self = shift;
+ return @$self ? $self->[0]->[TIME] : undef;
+}
+
+sub ARRAY__enqueue
+{
+ my $self = shift;
+ my ( $time, $code ) = @_;
+
+ # TODO: This could be more efficient maybe using a binary search
+ my $idx = 0;
+ $idx++ while $idx < @$self and $self->[$idx][TIME] <= $time;
+ splice @$self, $idx, 0, ( my $elem = [ $time, $code ]);
+
+ return $elem;
+}
+
+sub ARRAY_cancel
+{
+ my $self = shift;
+ my ( $id ) = @_;
+
+ @$self = grep { $_ != $id } @$self;
+}
+
+sub ARRAY__fire
+{
+ my $self = shift;
+ my ( $now ) = @_;
+
+ my $count = 0;
+
+ while( @$self ) {
+ last if( $self->[0]->[TIME] > $now );
+
+ my $top = shift @$self;
+
+ $top->[CODE]->();
+ $count++;
+ }
+
+ return $count;
+}
+
+# Implementation using Heap::Fibonacci
+
+sub HEAP_next_time
+{
+ my $self = shift;
+
+ my $top = $self->top;
+
+ return defined $top ? $top->time : undef;
+}
+
+sub HEAP__enqueue
+{
+ my $self = shift;
+ my ( $time, $code ) = @_;
+
+ my $elem = IO::Async::Internals::TimeQueue::Elem->new( $time, $code );
+ $self->add( $elem );
+
+ return $elem;
+}
+
+sub HEAP_cancel
+{
+ my $self = shift;
+ my ( $id ) = @_;
+
+ $self->delete( $id );
+}
+
+sub HEAP__fire
+{
+ my $self = shift;
+ my ( $now ) = @_;
+
+ my $count = 0;
+
+ while( defined( my $top = $self->top ) ) {
+ last if( $top->time > $now );
+
+ $self->extract_top;
+
+ $top->code->();
+ $count++;
+ }
+
+ return $count;
+}
+
+package # hide from CPAN
+ IO::Async::Internals::TimeQueue::Elem;
+
+use strict;
+our @ISA = qw( Heap::Elem );
+
+sub new
+{
+ my $self = shift;
+ my $class = ref $self || $self;
+
+ my ( $time, $code ) = @_;
+
+ my $new = $class->SUPER::new(
+ time => $time,
+ code => $code,
+ );
+
+ return $new;
+}
+
+sub time
+{
+ my $self = shift;
+ return $self->val->{time};
+}
+
+sub code
+{
+ my $self = shift;
+ return $self->val->{code};
+}
+
+# This only uses methods so is transparent to HASH or ARRAY
+sub cmp
+{
+ my $self = shift;
+ my $other = shift;
+
+ $self->time <=> $other->time;
+}
+
+0x55AA;
diff --git a/lib/IO/Async/Listener.pm b/lib/IO/Async/Listener.pm
new file mode 100644
index 0000000..277a9ab
--- /dev/null
+++ b/lib/IO/Async/Listener.pm
@@ -0,0 +1,549 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2008-2015 -- leonerd@leonerd.org.uk
+
+package IO::Async::Listener;
+
+use strict;
+use warnings;
+use base qw( IO::Async::Handle );
+
+our $VERSION = '0.67';
+
+use IO::Async::Handle;
+use IO::Async::OS;
+
+use Errno qw( EAGAIN EWOULDBLOCK );
+
+use Socket qw( sockaddr_family SOL_SOCKET SO_ACCEPTCONN SO_TYPE );
+
+use Carp;
+
+=head1 NAME
+
+C<IO::Async::Listener> - listen on network sockets for incoming connections
+
+=head1 SYNOPSIS
+
+ use IO::Async::Listener;
+
+ use IO::Async::Loop;
+ my $loop = IO::Async::Loop->new;
+
+ my $listener = IO::Async::Listener->new(
+ on_stream => sub {
+ my ( undef, $stream ) = @_;
+
+ $stream->configure(
+ on_read => sub {
+ my ( $self, $buffref, $eof ) = @_;
+ $self->write( $$buffref );
+ $$buffref = "";
+ return 0;
+ },
+ );
+
+ $loop->add( $stream );
+ },
+ );
+
+ $loop->add( $listener );
+
+ $listener->listen(
+ service => "echo",
+ socktype => 'stream',
+ )->get;
+
+ $loop->run;
+
+This object can also be used indirectly via an C<IO::Async::Loop>:
+
+ use IO::Async::Stream;
+
+ use IO::Async::Loop;
+ my $loop = IO::Async::Loop->new;
+
+ $loop->listen(
+ service => "echo",
+ socktype => 'stream',
+
+ on_stream => sub {
+ ...
+ },
+ )->get;
+
+ $loop->run;
+
+=head1 DESCRIPTION
+
+This subclass of L<IO::Async::Handle> adds behaviour which watches a socket in
+listening mode, to accept incoming connections on them.
+
+A Listener can be constructed and given a existing socket in listening mode.
+Alternatively, the Listener can construct a socket by calling the C<listen>
+method. Either a list of addresses can be provided, or a service name can be
+looked up using the underlying loop's C<resolve> method.
+
+=cut
+
+=head1 EVENTS
+
+The following events are invoked, either using subclass methods or CODE
+references in parameters:
+
+=head2 on_accept $clientsocket | $handle
+
+Invoked whenever a new client connects to the socket.
+
+If neither C<handle_constructor> nor C<handle_class> parameters are set, this
+will be invoked with the new client socket directly. If a handle constructor
+or class are set, this will be invoked with the newly-constructed handle,
+having the new socket already configured onto it.
+
+=head2 on_stream $stream
+
+An alternative to C<on_accept>, this an instance of L<IO::Async::Stream> when
+a new client connects. This is provided as a convenience for the common case
+that a Stream object is required as the transport for a Protocol object.
+
+This is now vaguely deprecated in favour of using C<on_accept> with a handle
+constructor or class.
+
+=head2 on_socket $socket
+
+Similar to C<on_stream>, but constructs an instance of L<IO::Async::Socket>.
+This is most useful for C<SOCK_DGRAM> or C<SOCK_RAW> sockets.
+
+This is now vaguely deprecated in favour of using C<on_accept> with a handle
+constructor or class.
+
+=head2 on_accept_error $socket, $errno
+
+Optional. Invoked if the C<accept> syscall indicates an error (other than
+C<EAGAIN> or C<EWOULDBLOCK>). If not provided, failures of C<accept> will
+be passed to the main C<on_error> handler.
+
+=cut
+
+=head1 PARAMETERS
+
+The following named parameters may be passed to C<new> or C<configure>:
+
+=head2 on_accept => CODE
+
+=head2 on_stream => CODE
+
+=head2 on_socket => CODE
+
+CODE reference for the event handlers. Because of the mutually-exclusive
+nature of their behaviour, only one of these may be set at a time. Setting one
+will remove the other two.
+
+=head2 handle => IO
+
+The IO handle containing an existing listen-mode socket.
+
+=head2 handle_constructor => CODE
+
+Optional. If defined, gives a CODE reference to be invoked every time a new
+client socket is accepted from the listening socket. It is passed the listener
+object itself, and is expected to return a new instance of
+C<IO::Async::Handle> or a subclass, used to wrap the new client socket.
+
+ $handle = $handle_constructor->( $listener )
+
+This can also be given as a subclass method
+
+ $handle = $listener->handle_constructor()
+
+=head2 handle_class => STRING
+
+Optional. If defined and C<handle_constructor> isn't, then new wrapper handles
+are constructed by invoking the C<new> method on the given class name, passing
+in no additional parameters.
+
+ $handle = $handle_class->new()
+
+This can also be given as a subclass method
+
+ $handle = $listener->handle_class->new
+
+=head2 acceptor => STRING|CODE
+
+Optional. If defined, gives the name of a method or a CODE reference to use to
+implement the actual accept behaviour. This will be invoked as:
+
+ ( $accepted ) = $listener->acceptor( $socket )->get
+
+ ( $handle ) = $listener->acceptor( $socket, handle => $handle )->get
+
+It is invoked with the listening socket as its its argument, and optionally
+an C<IO::Async::Handle> instance as a named parameter, and is expected to
+return a C<Future> that will eventually yield the newly-accepted socket or
+handle instance, if such was provided.
+
+=cut
+
+sub _init
+{
+ my $self = shift;
+ $self->SUPER::_init( @_ );
+
+ $self->{acceptor} = "_accept";
+}
+
+my @acceptor_events = qw( on_accept on_stream on_socket );
+
+sub configure
+{
+ my $self = shift;
+ my %params = @_;
+
+ if( grep exists $params{$_}, @acceptor_events ) {
+ grep( defined $_, @params{@acceptor_events} ) <= 1 or
+ croak "Can only set at most one of 'on_accept', 'on_stream' or 'on_socket'";
+
+ # Don't exists-test, so we'll clear the other two
+ $self->{$_} = delete $params{$_} for @acceptor_events;
+ }
+
+ croak "Cannot set 'on_read_ready' on a Listener" if exists $params{on_read_ready};
+
+ if( exists $params{handle} ) {
+ my $handle = delete $params{handle};
+ # Sanity check it - it may be a bare GLOB ref, not an IO::Socket-derived handle
+ defined getsockname( $handle ) or croak "IO handle $handle does not have a sockname";
+
+ # So now we know it's at least some kind of socket. Is it listening?
+ # SO_ACCEPTCONN would tell us, but not all OSes implement it. Since it's
+ # only a best-effort sanity check, we won't mind if the OS doesn't.
+ my $acceptconn = getsockopt( $handle, SOL_SOCKET, SO_ACCEPTCONN );
+ !defined $acceptconn or unpack( "I", $acceptconn ) or croak "Socket is not accepting connections";
+
+ # This is a bit naughty but hopefully nobody will mind...
+ bless $handle, "IO::Socket" if ref( $handle ) eq "GLOB";
+
+ $self->SUPER::configure( read_handle => $handle );
+ }
+
+ unless( grep $self->can_event( $_ ), @acceptor_events ) {
+ croak "Expected to be able to 'on_accept', 'on_stream' or 'on_socket'";
+ }
+
+ foreach (qw( acceptor handle_constructor handle_class )) {
+ $self->{$_} = delete $params{$_} if exists $params{$_};
+ }
+
+ if( keys %params ) {
+ croak "Cannot pass though configuration keys to underlying Handle - " . join( ", ", keys %params );
+ }
+}
+
+sub on_read_ready
+{
+ my $self = shift;
+
+ my $socket = $self->read_handle;
+
+ my $on_done;
+ my %acceptor_params;
+
+ if( $on_done = $self->can_event( "on_stream" ) ) {
+ # TODO: It doesn't make sense to put a SOCK_DGRAM in an
+ # IO::Async::Stream but currently we don't detect this
+ require IO::Async::Stream;
+ $acceptor_params{handle} = IO::Async::Stream->new;
+ }
+ elsif( $on_done = $self->can_event( "on_socket" ) ) {
+ require IO::Async::Socket;
+ $acceptor_params{handle} = IO::Async::Socket->new;
+ }
+ # on_accept needs to be last in case of multiple layers of subclassing
+ elsif( $on_done = $self->can_event( "on_accept" ) ) {
+ my $handle;
+
+ # Test both params before moving on to either method
+ if( my $constructor = $self->{handle_constructor} ) {
+ $handle = $self->{handle_constructor}->( $self );
+ }
+ elsif( my $class = $self->{handle_class} ) {
+ $handle = $class->new;
+ }
+ elsif( $self->can( "handle_constructor" ) ) {
+ $handle = $self->handle_constructor;
+ }
+ elsif( $self->can( "handle_class" ) ) {
+ $handle = $self->handle_class->new;
+ }
+
+ $acceptor_params{handle} = $handle if $handle;
+ }
+ else {
+ die "ARG! Missing on_accept,on_stream,on_socket!";
+ }
+
+ my $acceptor = $self->acceptor;
+ my $f = $self->$acceptor( $socket, %acceptor_params )->on_done( sub {
+ my ( $result ) = @_ or return; # false-alarm
+ $on_done->( $self, $result );
+ })->on_fail( sub {
+ my ( $message, $name, @args ) = @_;
+ if( $name eq "accept" ) {
+ my ( $socket, $dollarbang ) = @args;
+ $self->maybe_invoke_event( on_accept_error => $socket, $dollarbang ) or
+ $self->invoke_error( "accept() failed - $dollarbang", accept => $socket, $dollarbang );
+ }
+ });
+
+ # Caller is not going to keep hold of the Future, so we have to ensure it
+ # stays alive somehow
+ $self->adopt_future( $f->else( sub { Future->done } ) );
+}
+
+sub _accept
+{
+ my $self = shift;
+ my ( $listen_sock, %params ) = @_;
+
+ my $accepted = $listen_sock->accept;
+
+ if( defined $accepted ) {
+ $accepted->blocking( 0 );
+ if( my $handle = $params{handle} ) {
+ $handle->set_handle( $accepted );
+ return Future->done( $handle );
+ }
+ else {
+ return Future->done( $accepted );
+ }
+ }
+ elsif( $! == EAGAIN or $! == EWOULDBLOCK ) {
+ return Future->done;
+ }
+ else {
+ return Future->fail( "Cannot accept() - $!", accept => $listen_sock, $! );
+ }
+}
+
+=head1 METHODS
+
+The following methods documented with a trailing call to C<< ->get >> return
+L<Future> instances.
+
+=cut
+
+=head2 $acceptor = $listener->acceptor
+
+Returns the currently-set C<acceptor> method name or code reference. This may
+be of interest to Loop C<listen> extension methods that wish to extend or wrap
+it.
+
+=cut
+
+sub acceptor
+{
+ my $self = shift;
+ return $self->{acceptor};
+}
+
+sub is_listening
+{
+ my $self = shift;
+
+ return ( defined $self->sockname );
+}
+
+=head2 $name = $listener->sockname
+
+Returns the C<sockname> of the underlying listening socket
+
+=cut
+
+sub sockname
+{
+ my $self = shift;
+
+ my $handle = $self->read_handle or return undef;
+ return $handle->sockname;
+}
+
+=head2 $family = $listener->family
+
+Returns the socket address family of the underlying listening socket
+
+=cut
+
+sub family
+{
+ my $self = shift;
+
+ my $sockname = $self->sockname or return undef;
+ return sockaddr_family( $sockname );
+}
+
+=head2 $socktype = $listener->socktype
+
+Returns the socket type of the underlying listening socket
+
+=cut
+
+sub socktype
+{
+ my $self = shift;
+
+ my $handle = $self->read_handle or return undef;
+ return $handle->sockopt(SO_TYPE);
+}
+
+=head2 $listener->listen( %params )
+
+This method sets up a listening socket and arranges for the acceptor callback
+to be invoked each time a new connection is accepted on the socket.
+
+Most parameters given to this method are passed into the C<listen> method of
+the L<IO::Async::Loop> object. In addition, the following arguments are also
+recognised directly:
+
+=over 8
+
+=item on_listen => CODE
+
+Optional. A callback that is invoked when the listening socket is ready.
+Similar to that on the underlying loop method, except it is passed the
+listener object itself.
+
+ $on_listen->( $listener )
+
+=back
+
+=cut
+
+sub listen
+{
+ my $self = shift;
+ my ( %params ) = @_;
+
+ my $loop = $self->loop;
+ defined $loop or croak "Cannot listen when not a member of a Loop"; # TODO: defer?
+
+ if( my $on_listen = delete $params{on_listen} ) {
+ $params{on_listen} = sub { $on_listen->( $self ) };
+ }
+
+ $loop->listen( listener => $self, %params );
+}
+
+=head1 EXAMPLES
+
+=head2 Listening on UNIX Sockets
+
+The C<handle> argument can be passed an existing socket already in listening
+mode, making it possible to listen on other types of socket such as UNIX
+sockets.
+
+ use IO::Async::Listener;
+ use IO::Socket::UNIX;
+
+ use IO::Async::Loop;
+ my $loop = IO::Async::Loop->new;
+
+ my $listener = IO::Async::Listener->new(
+ on_stream => sub {
+ my ( undef, $stream ) = @_;
+
+ $stream->configure(
+ on_read => sub {
+ my ( $self, $buffref, $eof ) = @_;
+ $self->write( $$buffref );
+ $$buffref = "";
+ return 0;
+ },
+ );
+
+ $loop->add( $stream );
+ },
+ );
+
+ $loop->add( $listener );
+
+ my $socket = IO::Socket::UNIX->new(
+ Local => "echo.sock",
+ Listen => 1,
+ ) or die "Cannot make UNIX socket - $!\n";
+
+ $listener->listen(
+ handle => $socket,
+ );
+
+ $loop->run;
+
+=head2 Passing Plain Socket Addresses
+
+The C<addr> or C<addrs> parameters should contain a definition of a plain
+socket address in a form that the L<IO::Async::OS> C<extract_addrinfo>
+method can use.
+
+This example shows how to listen on TCP port 8001 on address 10.0.0.1:
+
+ $listener->listen(
+ addr => {
+ family => "inet",
+ socktype => "stream",
+ port => 8001,
+ ip => "10.0.0.1",
+ },
+ ...
+ );
+
+This example shows another way to listen on a UNIX socket, similar to the
+earlier example:
+
+ $listener->listen(
+ addr => {
+ family => "unix",
+ socktype => "stream",
+ path => "echo.sock",
+ },
+ ...
+ );
+
+=head2 Using A Kernel-Assigned Port Number
+
+Rather than picking a specific port number, is it possible to ask the kernel
+to assign one arbitrarily that is currently free. This can be done by
+requesting port number 0 (which is actually the default if no port number is
+otherwise specified). To determine which port number the kernel actually
+picked, inspect the C<sockport> accessor on the actual socket filehandle.
+
+Either use the L<Future> returned by the C<listen> method:
+
+ $listener->listen(
+ addr => { family => "inet" },
+ )->on_done( sub {
+ my ( $listener ) = @_;
+ my $socket = $listener->read_handle;
+
+ say "Now listening on port ", $socket->sockport;
+ });
+
+Or pass an C<on_listen> continuation:
+
+ $listener->listen(
+ addr => { family => "inet" },
+
+ on_listen => sub {
+ my ( $listener ) = @_;
+ my $socket = $listener->read_handle;
+
+ say "Now listening on port ", $socket->sockport;
+ },
+ );
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/Loop.pm b/lib/IO/Async/Loop.pm
new file mode 100644
index 0000000..e510c64
--- /dev/null
+++ b/lib/IO/Async/Loop.pm
@@ -0,0 +1,2781 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2007-2015 -- leonerd@leonerd.org.uk
+
+package IO::Async::Loop;
+
+use strict;
+use warnings;
+use 5.010;
+
+our $VERSION = '0.67';
+
+# When editing this value don't forget to update the docs below
+use constant NEED_API_VERSION => '0.33';
+
+# Base value but some classes might override
+use constant _CAN_ON_HANGUP => 0;
+
+# Most Loop implementations do not accurately handle sub-second timers.
+# This only matters for unit tests
+use constant _CAN_SUBSECOND_ACCURATELY => 0;
+
+# Does the loop implementation support IO_ASYNC_WATCHDOG?
+use constant _CAN_WATCHDOG => 0;
+
+# Watchdog configuration constants
+use constant WATCHDOG_ENABLE => $ENV{IO_ASYNC_WATCHDOG};
+use constant WATCHDOG_INTERVAL => $ENV{IO_ASYNC_WATCHDOG_INTERVAL} || 10;
+use constant WATCHDOG_SIGABRT => $ENV{IO_ASYNC_WATCHDOG_SIGABRT};
+
+use Carp;
+
+use IO::Socket (); # empty import
+use Time::HiRes qw(); # empty import
+use POSIX qw( WNOHANG );
+use Scalar::Util qw( refaddr weaken );
+use Socket qw( SO_REUSEADDR AF_INET6 IPPROTO_IPV6 IPV6_V6ONLY );
+
+use IO::Async::OS;
+
+use constant HAVE_SIGNALS => IO::Async::OS->HAVE_SIGNALS;
+use constant HAVE_POSIX_FORK => IO::Async::OS->HAVE_POSIX_FORK;
+use constant HAVE_THREADS => IO::Async::OS->HAVE_THREADS;
+
+# Never sleep for more than 1 second if a signal proxy is registered, to avoid
+# a borderline race condition.
+# There is a race condition in perl involving signals interacting with XS code
+# that implements blocking syscalls. There is a slight chance a signal will
+# arrive in the XS function, before the blocking itself. Perl will not run our
+# (safe) deferred signal handler in this case. To mitigate this, if we have a
+# signal proxy, we'll adjust the maximal timeout. The signal handler will be
+# run when the XS function returns.
+our $MAX_SIGWAIT_TIME = 1;
+
+# Also, never sleep for more than 1 second if the OS does not support signals
+# and we have child watches registered (so we must use waitpid() polling)
+our $MAX_CHILDWAIT_TIME = 1;
+
+# Maybe our calling program will have a suggested hint of a specific Loop
+# class or list of classes to use
+our $LOOP;
+
+# Undocumented; used only by the test scripts.
+# Setting this value true will avoid the IO::Async::Loop::$^O candidate in the
+# magic constructor
+our $LOOP_NO_OS;
+
+# SIGALRM handler for watchdog
+$SIG{ALRM} = sub {
+ # There are two extra frames here; this one and the signal handler itself
+ local $Carp::CarpLevel = $Carp::CarpLevel + 2;
+ if( WATCHDOG_SIGABRT ) {
+ print STDERR Carp::longmess( "Watchdog timeout" );
+ kill ABRT => $$;
+ }
+ else {
+ Carp::confess( "Watchdog timeout" );
+ }
+} if WATCHDOG_ENABLE;
+
+$SIG{PIPE} = "IGNORE" if ( $SIG{PIPE} // "" ) eq "DEFAULT";
+
+=head1 NAME
+
+C<IO::Async::Loop> - core loop of the C<IO::Async> framework
+
+=head1 SYNOPSIS
+
+ use IO::Async::Stream;
+ use IO::Async::Timer::Countdown;
+
+ use IO::Async::Loop;
+
+ my $loop = IO::Async::Loop->new;
+
+ $loop->add( IO::Async::Timer::Countdown->new(
+ delay => 10,
+ on_expire => sub { print "10 seconds have passed\n" },
+ )->start );
+
+ $loop->add( IO::Async::Stream->new_for_stdin(
+ on_read => sub {
+ my ( $self, $buffref, $eof ) = @_;
+
+ while( $$buffref =~ s/^(.*)\n// ) {
+ print "You typed a line $1\n";
+ }
+
+ return 0;
+ },
+ ) );
+
+ $loop->run;
+
+=head1 DESCRIPTION
+
+This module provides an abstract class which implements the core loop of the
+C<IO::Async> framework. Its primary purpose is to store a set of
+L<IO::Async::Notifier> objects or subclasses of them. It handles all of the
+lower-level set manipulation actions, and leaves the actual IO readiness
+testing/notification to the concrete class that implements it. It also
+provides other functionality such as signal handling, child process managing,
+and timers.
+
+See also the two bundled Loop subclasses:
+
+=over 4
+
+=item L<IO::Async::Loop::Select>
+
+=item L<IO::Async::Loop::Poll>
+
+=back
+
+Or other subclasses that may appear on CPAN which are not part of the core
+C<IO::Async> distribution.
+
+=head2 Ignoring SIGPIPE
+
+Since version I<0.66> loading this module automatically ignores C<SIGPIPE>, as
+it is highly unlikely that the default-terminate action is the best course of
+action for an C<IO::Async>-based program to take. If at load time the handler
+disposition is still set as C<DEFAULT>, it is set to ignore. If already
+another handler has been placed there by the program code, it will be left
+undisturbed.
+
+=cut
+
+# Internal constructor used by subclasses
+sub __new
+{
+ my $class = shift;
+
+ # Detect if the API version provided by the subclass is sufficient
+ $class->can( "API_VERSION" ) or
+ die "$class is too old for IO::Async $VERSION; it does not provide \->API_VERSION\n";
+
+ $class->API_VERSION >= NEED_API_VERSION or
+ die "$class is too old for IO::Async $VERSION; we need API version >= ".NEED_API_VERSION.", it provides ".$class->API_VERSION."\n";
+
+ WATCHDOG_ENABLE and !$class->_CAN_WATCHDOG and
+ warn "$class cannot implement IO_ASYNC_WATCHDOG\n";
+
+ my $self = bless {
+ notifiers => {}, # {nkey} = notifier
+ iowatches => {}, # {fd} = [ $on_read_ready, $on_write_ready, $on_hangup ]
+ sigattaches => {}, # {sig} => \@callbacks
+ childmanager => undef,
+ childwatches => {}, # {pid} => $code
+ threadwatches => {}, # {tid} => $code
+ timequeue => undef,
+ deferrals => [],
+ os => {}, # A generic scratchpad for IO::Async::OS to store whatever it wants
+ }, $class;
+
+ # It's possible this is a specific subclass constructor. We still want the
+ # magic IO::Async::Loop->new constructor to yield this if it's the first
+ # one
+ our $ONE_TRUE_LOOP ||= $self;
+
+ # Legacy support - temporary until all CPAN classes are updated; bump NEEDAPI version at that point
+ my $old_timer = $self->can( "enqueue_timer" ) != \&enqueue_timer;
+ if( $old_timer != ( $self->can( "cancel_timer" ) != \&cancel_timer ) ) {
+ die "$class should overload both ->enqueue_timer and ->cancel_timer, or neither";
+ }
+
+ if( $old_timer ) {
+ warnings::warnif( deprecated => "Enabling old_timer workaround for old loop class " . $class );
+ }
+
+ $self->{old_timer} = $old_timer;
+
+ return $self;
+}
+
+=head1 MAGIC CONSTRUCTOR
+
+=head2 $loop = IO::Async::Loop->new
+
+This function attempts to find a good subclass to use, then calls its
+constructor. It works by making a list of likely candidate classes, then
+trying each one in turn, C<require>ing the module then calling its C<new>
+method. If either of these operations fails, the next subclass is tried. If
+no class was successful, then an exception is thrown.
+
+The constructed object is cached, and will be returned again by a subsequent
+call. The cache will also be set by a constructor on a specific subclass. This
+behaviour makes it possible to simply use the normal constructor in a module
+that wishes to interract with the main program's Loop, such as an integration
+module for another event system.
+
+For example, the following two C<$loop> variables will refer to the same
+object:
+
+ use IO::Async::Loop;
+ use IO::Async::Loop::Poll;
+
+ my $loop_poll = IO::Async::Loop::Poll->new;
+
+ my $loop = IO::Async::Loop->new;
+
+While it is not advised to do so under normal circumstances, if the program
+really wishes to construct more than one Loop object, it can call the
+constructor C<really_new>, or invoke one of the subclass-specific constructors
+directly.
+
+The list of candidates is formed from the following choices, in this order:
+
+=over 4
+
+=item * $ENV{IO_ASYNC_LOOP}
+
+If this environment variable is set, it should contain a comma-separated list
+of subclass names. These names may or may not be fully-qualified; if a name
+does not contain C<::> then it will have C<IO::Async::Loop::> prepended to it.
+This allows the end-user to specify a particular choice to fit the needs of
+his use of a program using C<IO::Async>.
+
+=item * $IO::Async::Loop::LOOP
+
+If this scalar is set, it should contain a comma-separated list of subclass
+names. These may or may not be fully-qualified, as with the above case. This
+allows a program author to suggest a loop module to use.
+
+In cases where the module subclass is a hard requirement, such as GTK programs
+using C<Glib>, it would be better to use the module specifically and invoke
+its constructor directly.
+
+=item * IO::Async::OS->LOOP_PREFER_CLASSES
+
+The L<IO::Async::OS> hints module for the given OS is then consulted to see if
+it suggests any other module classes specific to the given operating system.
+
+=item * $^O
+
+The module called C<IO::Async::Loop::$^O> is tried next. This allows specific
+OSes, such as the ever-tricky C<MSWin32>, to provide an implementation that
+might be more efficient than the generic ones, or even work at all.
+
+This option is now discouraged in favour of the C<IO::Async::OS> hint instead.
+At some future point it may be removed entirely, given as currently only
+C<linux> uses it.
+
+=item * Poll and Select
+
+Finally, if no other choice has been made by now, the built-in C<Poll> module
+is chosen. This should always work, but in case it doesn't, the C<Select>
+module will be chosen afterwards as a last-case attempt. If this also fails,
+then the magic constructor itself will throw an exception.
+
+=back
+
+If any of the explicitly-requested loop types (C<$ENV{IO_ASYNC_LOOP}> or
+C<$IO::Async::Loop::LOOP>) fails to load then a warning is printed detailing
+the error.
+
+Implementors of new C<IO::Async::Loop> subclasses should see the notes about
+C<API_VERSION> below.
+
+=cut
+
+sub __try_new
+{
+ my ( $class ) = @_;
+
+ ( my $file = "$class.pm" ) =~ s{::}{/}g;
+
+ eval {
+ local $SIG{__WARN__} = sub {};
+ require $file;
+ } or return;
+
+ my $self;
+ $self = eval { $class->new } and return $self;
+
+ # Oh dear. We've loaded the code OK but for some reason the constructor
+ # wasn't happy. Being polite we ought really to unload the file again,
+ # but perl doesn't actually provide us a way to do this.
+
+ return undef;
+}
+
+sub new
+{
+ return our $ONE_TRUE_LOOP ||= shift->really_new;
+}
+
+# Ensure that the loop is DESTROYed recursively at exit time, before GD happens
+END {
+ undef our $ONE_TRUE_LOOP;
+}
+
+sub really_new
+{
+ shift; # We're going to ignore the class name actually given
+ my $self;
+
+ my @candidates;
+
+ push @candidates, split( m/,/, $ENV{IO_ASYNC_LOOP} ) if defined $ENV{IO_ASYNC_LOOP};
+
+ push @candidates, split( m/,/, $LOOP ) if defined $LOOP;
+
+ foreach my $class ( @candidates ) {
+ $class =~ m/::/ or $class = "IO::Async::Loop::$class";
+ $self = __try_new( $class ) and return $self;
+
+ my ( $topline ) = split m/\n/, $@; # Ignore all the other lines; they'll be require's verbose output
+ warn "Unable to use $class - $topline\n";
+ }
+
+ unless( $LOOP_NO_OS ) {
+ foreach my $class ( IO::Async::OS->LOOP_PREFER_CLASSES, "IO::Async::Loop::$^O" ) {
+ $class =~ m/::/ or $class = "IO::Async::Loop::$class";
+ $self = __try_new( $class ) and return $self;
+
+ # Don't complain about these ones
+ }
+ }
+
+ return IO::Async::Loop->new_builtin;
+}
+
+sub new_builtin
+{
+ shift;
+ my $self;
+
+ foreach my $class ( IO::Async::OS->LOOP_BUILTIN_CLASSES ) {
+ $self = __try_new( "IO::Async::Loop::$class" ) and return $self;
+ }
+
+ croak "Cannot find a suitable candidate class";
+}
+
+#######################
+# Notifier management #
+#######################
+
+=head1 NOTIFIER MANAGEMENT
+
+The following methods manage the collection of C<IO::Async::Notifier> objects.
+
+=cut
+
+=head2 $loop->add( $notifier )
+
+This method adds another notifier object to the stored collection. The object
+may be a C<IO::Async::Notifier>, or any subclass of it.
+
+When a notifier is added, any children it has are also added, recursively. In
+this way, entire sections of a program may be written within a tree of
+notifier objects, and added or removed on one piece.
+
+=cut
+
+sub add
+{
+ my $self = shift;
+ my ( $notifier ) = @_;
+
+ if( defined $notifier->parent ) {
+ croak "Cannot add a child notifier directly - add its parent";
+ }
+
+ if( defined $notifier->loop ) {
+ croak "Cannot add a notifier that is already a member of a loop";
+ }
+
+ $self->_add_noparentcheck( $notifier );
+}
+
+sub _add_noparentcheck
+{
+ my $self = shift;
+ my ( $notifier ) = @_;
+
+ my $nkey = refaddr $notifier;
+
+ $self->{notifiers}->{$nkey} = $notifier;
+
+ $notifier->__set_loop( $self );
+
+ $self->_add_noparentcheck( $_ ) for $notifier->children;
+
+ return;
+}
+
+=head2 $loop->remove( $notifier )
+
+This method removes a notifier object from the stored collection, and
+recursively and children notifiers it contains.
+
+=cut
+
+sub remove
+{
+ my $self = shift;
+ my ( $notifier ) = @_;
+
+ if( defined $notifier->parent ) {
+ croak "Cannot remove a child notifier directly - remove its parent";
+ }
+
+ $self->_remove_noparentcheck( $notifier );
+}
+
+sub _remove_noparentcheck
+{
+ my $self = shift;
+ my ( $notifier ) = @_;
+
+ my $nkey = refaddr $notifier;
+
+ exists $self->{notifiers}->{$nkey} or croak "Notifier does not exist in collection";
+
+ delete $self->{notifiers}->{$nkey};
+
+ $notifier->__set_loop( undef );
+
+ $self->_remove_noparentcheck( $_ ) for $notifier->children;
+
+ return;
+}
+
+=head2 @notifiers = $loop->notifiers
+
+Returns a list of all the notifier objects currently stored in the Loop.
+
+=cut
+
+sub notifiers
+{
+ my $self = shift;
+ # Sort so the order remains stable under additions/removals
+ return map { $self->{notifiers}->{$_} } sort keys %{ $self->{notifiers} };
+}
+
+###################
+# Looping support #
+###################
+
+=head1 LOOPING CONTROL
+
+The following methods control the actual run cycle of the loop, and hence the
+program.
+
+=cut
+
+=head2 $count = $loop->loop_once( $timeout )
+
+This method performs a single wait loop using the specific subclass's
+underlying mechanism. If C<$timeout> is undef, then no timeout is applied, and
+it will wait until an event occurs. The intention of the return value is to
+indicate the number of callbacks that this loop executed, though different
+subclasses vary in how accurately they can report this. See the documentation
+for this method in the specific subclass for more information.
+
+=cut
+
+sub loop_once
+{
+ my $self = shift;
+ my ( $timeout ) = @_;
+
+ croak "Expected that $self overrides ->loop_once";
+}
+
+=head2 @result = $loop->run
+
+=head2 $result = $loop->run
+
+Runs the actual IO event loop. This method blocks until the C<stop> method is
+called, and returns the result that was passed to C<stop>. In scalar context
+only the first result is returned; the others will be discarded if more than
+one value was provided. This method may be called recursively.
+
+This method is a recent addition and may not be supported by all the
+C<IO::Async::Loop> subclasses currently available on CPAN.
+
+=cut
+
+sub run
+{
+ my $self = shift;
+
+ local $self->{running} = 1;
+ local $self->{result} = [];
+
+ while( $self->{running} ) {
+ $self->loop_once( undef );
+ }
+
+ return wantarray ? @{ $self->{result} } : $self->{result}[0];
+}
+
+=head2 $loop->stop( @result )
+
+Stops the inner-most C<run> method currently in progress, causing it to return
+the given C<@result>.
+
+This method is a recent addition and may not be supported by all the
+C<IO::Async::Loop> subclasses currently available on CPAN.
+
+=cut
+
+sub stop
+{
+ my $self = shift;
+
+ @{ $self->{result} } = @_;
+ undef $self->{running};
+}
+
+=head2 $loop->loop_forever
+
+A synonym for C<run>, though this method does not return a result.
+
+=cut
+
+sub loop_forever
+{
+ my $self = shift;
+ $self->run;
+ return;
+}
+
+=head2 $loop->loop_stop
+
+A synonym for C<stop>, though this method does not pass any results.
+
+=cut
+
+sub loop_stop
+{
+ my $self = shift;
+ $self->stop;
+}
+
+=head2 $loop->post_fork
+
+The base implementation of this method does nothing. It is provided in case
+some Loop subclasses should take special measures after a C<fork()> system
+call if the main body of the program should survive in both running processes.
+
+This may be required, for example, in a long-running server daemon that forks
+multiple copies on startup after opening initial listening sockets. A loop
+implementation that uses some in-kernel resource that becomes shared after
+forking (for example, a Linux C<epoll> or a BSD C<kqueue> filehandle) would
+need recreating in the new child process before the program can continue.
+
+=cut
+
+sub post_fork
+{
+ # empty
+}
+
+###########
+# Futures #
+###########
+
+=head1 FUTURE SUPPORT
+
+The following methods relate to L<IO::Async::Future> objects.
+
+=cut
+
+=head2 $future = $loop->new_future
+
+Returns a new C<IO::Async::Future> instance with a reference to the Loop.
+
+=cut
+
+sub new_future
+{
+ my $self = shift;
+ require IO::Async::Future;
+ return IO::Async::Future->new( $self );
+}
+
+=head2 $loop->await( $future )
+
+Blocks until the given future is ready, as indicated by its C<is_ready> method.
+As a convenience it returns the future, to simplify code:
+
+ my @result = $loop->await( $future )->get;
+
+=cut
+
+sub await
+{
+ my $self = shift;
+ my ( $future ) = @_;
+
+ $self->loop_once until $future->is_ready;
+
+ return $future;
+}
+
+=head2 $loop->await_all( @futures )
+
+Blocks until all the given futures are ready, as indicated by the C<is_ready>
+method. Equivalent to calling C<await> on a C<< Future->wait_all >> except
+that it doesn't create the surrounding future object.
+
+=cut
+
+sub _all_ready { $_->is_ready or return 0 for @_; return 1 }
+
+sub await_all
+{
+ my $self = shift;
+ my @futures = @_;
+
+ $self->loop_once until _all_ready @futures;
+}
+
+=head2 $loop->delay_future( %args )->get
+
+Returns a new C<IO::Async::Future> instance which will become done at a given
+point in time. The C<%args> should contain an C<at> or C<after> key as per the
+C<watch_time> method. The returned future may be cancelled to cancel the
+timer. At the alloted time the future will succeed with an empty result list.
+
+=cut
+
+sub delay_future
+{
+ my $self = shift;
+ my %args = @_;
+
+ my $future = $self->new_future;
+ my $id = $self->watch_time( %args,
+ code => sub { $future->done },
+ );
+
+ $future->on_cancel( sub { shift->loop->unwatch_time( $id ) } );
+
+ return $future;
+}
+
+=head2 $loop->timeout_future( %args )->get
+
+Returns a new C<IO::Async::Future> instance which will fail at a given point
+in time. The C<%args> should contain an C<at> or C<after> key as per the
+C<watch_time> method. The returned future may be cancelled to cancel the
+timer. At the alloted time, the future will fail with the string C<"Timeout">.
+
+=cut
+
+sub timeout_future
+{
+ my $self = shift;
+ my %args = @_;
+
+ my $future = $self->new_future;
+ my $id = $self->watch_time( %args,
+ code => sub { $future->fail( "Timeout" ) },
+ );
+
+ $future->on_cancel( sub { shift->loop->unwatch_time( $id ) } );
+
+ return $future;
+}
+
+############
+# Features #
+############
+
+=head1 FEATURES
+
+Most of the following methods are higher-level wrappers around base
+functionality provided by the low-level API documented below. They may be
+used by C<IO::Async::Notifier> subclasses or called directly by the program.
+
+The following methods documented with a trailing call to C<< ->get >> return
+L<Future> instances.
+
+=cut
+
+sub __new_feature
+{
+ my $self = shift;
+ my ( $classname ) = @_;
+
+ ( my $filename = "$classname.pm" ) =~ s{::}{/}g;
+ require $filename;
+
+ # These features aren't supposed to be "user visible", so if methods called
+ # on it carp or croak, the shortmess line ought to skip IO::Async::Loop and
+ # go on report its caller. To make this work, add the feature class to our
+ # @CARP_NOT list.
+ push our(@CARP_NOT), $classname;
+
+ return $classname->new( loop => $self );
+}
+
+=head2 $id = $loop->attach_signal( $signal, $code )
+
+This method adds a new signal handler to watch the given signal. The same
+signal can be attached to multiple times; its callback functions will all be
+invoked, in no particular order.
+
+The returned C<$id> value can be used to identify the signal handler in case
+it needs to be removed by the C<detach_signal> method. Note that this value
+may be an object reference, so if it is stored, it should be released after it
+cancelled, so the object itself can be freed.
+
+=over 8
+
+=item $signal
+
+The name of the signal to attach to. This should be a bare name like C<TERM>.
+
+=item $code
+
+A CODE reference to the handling callback.
+
+=back
+
+Attaching to C<SIGCHLD> is not recommended because of the way all child
+processes use it to report their termination. Instead, the C<watch_child>
+method should be used to watch for termination of a given child process. A
+warning will be printed if C<SIGCHLD> is passed here, but in future versions
+of C<IO::Async> this behaviour may be disallowed altogether.
+
+See also L<POSIX> for the C<SIGI<name>> constants.
+
+For a more flexible way to use signals from within Notifiers, see instead the
+L<IO::Async::Signal> object.
+
+=cut
+
+sub attach_signal
+{
+ my $self = shift;
+ my ( $signal, $code ) = @_;
+
+ HAVE_SIGNALS or croak "This OS cannot ->attach_signal";
+
+ if( $signal eq "CHLD" ) {
+ # We make special exception to allow $self->watch_child to do this
+ caller eq "IO::Async::Loop" or
+ carp "Attaching to SIGCHLD is not advised - use ->watch_child instead";
+ }
+
+ if( not $self->{sigattaches}->{$signal} ) {
+ my @attaches;
+ $self->watch_signal( $signal, sub {
+ foreach my $attachment ( @attaches ) {
+ $attachment->();
+ }
+ } );
+ $self->{sigattaches}->{$signal} = \@attaches;
+ }
+
+ push @{ $self->{sigattaches}->{$signal} }, $code;
+
+ return \$self->{sigattaches}->{$signal}->[-1];
+}
+
+=head2 $loop->detach_signal( $signal, $id )
+
+Removes a previously-attached signal handler.
+
+=over 8
+
+=item $signal
+
+The name of the signal to remove from. This should be a bare name like
+C<TERM>.
+
+=item $id
+
+The value returned by the C<attach_signal> method.
+
+=back
+
+=cut
+
+sub detach_signal
+{
+ my $self = shift;
+ my ( $signal, $id ) = @_;
+
+ HAVE_SIGNALS or croak "This OS cannot ->detach_signal";
+
+ # Can't use grep because we have to preserve the addresses
+ my $attaches = $self->{sigattaches}->{$signal} or return;
+
+ for (my $i = 0; $i < @$attaches; ) {
+ $i++, next unless \$attaches->[$i] == $id;
+
+ splice @$attaches, $i, 1, ();
+ }
+
+ if( !@$attaches ) {
+ $self->unwatch_signal( $signal );
+ delete $self->{sigattaches}->{$signal};
+ }
+}
+
+=head2 $loop->later( $code )
+
+Schedules a code reference to be invoked as soon as the current round of IO
+operations is complete.
+
+The code reference is never invoked immediately, though the loop will not
+perform any blocking operations between when it is installed and when it is
+invoked. It may call C<select>, C<poll> or equivalent with a zero-second
+timeout, and process any currently-pending IO conditions before the code is
+invoked, but it will not block for a non-zero amount of time.
+
+This method is implemented using the C<watch_idle> method, with the C<when>
+parameter set to C<later>. It will return an ID value that can be passed to
+C<unwatch_idle> if required.
+
+=cut
+
+sub later
+{
+ my $self = shift;
+ my ( $code ) = @_;
+
+ return $self->watch_idle( when => 'later', code => $code );
+}
+
+=head2 $loop->spawn_child( %params )
+
+This method creates a new child process to run a given code block or command.
+For more detail, see the C<spawn_child> method on the
+L<IO::Async::ChildManager> class.
+
+=cut
+
+sub spawn_child
+{
+ my $self = shift;
+ my %params = @_;
+
+ my $childmanager = $self->{childmanager} ||=
+ $self->__new_feature( "IO::Async::ChildManager" );
+
+ $childmanager->spawn_child( %params );
+}
+
+=head2 $pid = $loop->open_child( %params )
+
+This creates a new child process to run the given code block or command, and
+attaches filehandles to it that the parent will watch. This method is a light
+wrapper around constructing a new L<IO::Async::Process> object, provided
+largely for backward compatibility. New code ought to construct such an object
+directly, as it may provide more features than are available here.
+
+The C<%params> hash takes the following keys:
+
+=over 8
+
+=item command => ARRAY or STRING
+
+=item code => CODE
+
+The command or code to run in the child process (as per the C<spawn> method)
+
+=item on_finish => CODE
+
+A continuation to be called when the child process exits and has closed all of
+the filehandles that were set up for it. It will be invoked in the following
+way:
+
+ $on_finish->( $pid, $exitcode )
+
+The second argument is passed the plain perl C<$?> value.
+
+=item on_error => CODE
+
+Optional continuation to be called when the child code block throws an
+exception, or the command could not be C<exec(2)>ed. It will be invoked in the
+following way (as per C<spawn>)
+
+ $on_error->( $pid, $exitcode, $dollarbang, $dollarat )
+
+If this continuation is not supplied, then C<on_finish> is used instead. The
+value of C<$!> and C<$@> will not be reported.
+
+=item setup => ARRAY
+
+Optional reference to an array to pass to the underlying C<spawn> method.
+
+=back
+
+In addition, the hash takes keys that define how to set up file descriptors in
+the child process. (If the C<setup> array is also given, these operations will
+be performed after those specified by C<setup>.)
+
+=over 8
+
+=item fdI<n> => HASH
+
+A hash describing how to set up file descriptor I<n>. The hash may contain one
+of the following sets of keys:
+
+=over 4
+
+=item on_read => CODE
+
+The child will be given the writing end of a pipe. The reading end will be
+wrapped by an C<IO::Async::Stream> using this C<on_read> callback function.
+
+=item from => STRING
+
+The child will be given the reading end of a pipe. The string given by the
+C<from> parameter will be written to the child. When all of the data has been
+written the pipe will be closed.
+
+=back
+
+=item stdin => ...
+
+=item stdout => ...
+
+=item stderr => ...
+
+Shortcuts for C<fd0>, C<fd1> and C<fd2> respectively.
+
+=back
+
+=cut
+
+sub open_child
+{
+ my $self = shift;
+ my %params = @_;
+
+ my $on_finish = delete $params{on_finish};
+ ref $on_finish or croak "Expected 'on_finish' to be a reference";
+ $params{on_finish} = sub {
+ my ( $process, $exitcode ) = @_;
+ $on_finish->( $process->pid, $exitcode );
+ };
+
+ if( my $on_error = delete $params{on_error} ) {
+ ref $on_error or croak "Expected 'on_error' to be a reference";
+
+ $params{on_exception} = sub {
+ my ( $process, $exception, $errno, $exitcode ) = @_;
+ # Swap order
+ $on_error->( $process->pid, $exitcode, $errno, $exception );
+ };
+ }
+
+ $params{on_exit} and croak "Cannot pass 'on_exit' parameter through ChildManager->open";
+
+ require IO::Async::Process;
+ my $process = IO::Async::Process->new( %params );
+
+ $self->add( $process );
+
+ return $process->pid;
+}
+
+=head2 $pid = $loop->run_child( %params )
+
+This creates a new child process to run the given code block or command,
+capturing its STDOUT and STDERR streams. When the process exits, a
+continuation is invoked being passed the exitcode, and content of the streams.
+
+=over 8
+
+=item command => ARRAY or STRING
+
+=item code => CODE
+
+The command or code to run in the child process (as per the C<spawn_child>
+method)
+
+=item on_finish => CODE
+
+A continuation to be called when the child process exits and closed its STDOUT
+and STDERR streams. It will be invoked in the following way:
+
+ $on_finish->( $pid, $exitcode, $stdout, $stderr )
+
+The second argument is passed the plain perl C<$?> value.
+
+=item stdin => STRING
+
+Optional. String to pass in to the child process's STDIN stream.
+
+=item setup => ARRAY
+
+Optional reference to an array to pass to the underlying C<spawn> method.
+
+=back
+
+This method is intended mainly as an IO::Async-compatible replacement for the
+perl C<readpipe> function (`backticks`), allowing it to replace
+
+ my $output = `command here`;
+
+with
+
+ $loop->run_child(
+ command => "command here",
+ on_finish => sub {
+ my ( undef, $exitcode, $output ) = @_;
+ ...
+ }
+ );
+
+=cut
+
+sub run_child
+{
+ my $self = shift;
+ my %params = @_;
+
+ my $on_finish = delete $params{on_finish};
+ ref $on_finish or croak "Expected 'on_finish' to be a reference";
+
+ my $stdout;
+ my $stderr;
+
+ my %subparams;
+
+ if( my $child_stdin = delete $params{stdin} ) {
+ ref $child_stdin and croak "Expected 'stdin' not to be a reference";
+ $subparams{stdin} = { from => $child_stdin };
+ }
+
+ $subparams{code} = delete $params{code};
+ $subparams{command} = delete $params{command};
+ $subparams{setup} = delete $params{setup};
+
+ croak "Unrecognised parameters " . join( ", ", keys %params ) if keys %params;
+
+ require IO::Async::Process;
+ my $process = IO::Async::Process->new(
+ %subparams,
+ stdout => { into => \$stdout },
+ stderr => { into => \$stderr },
+
+ on_finish => sub {
+ my ( $process, $exitcode ) = @_;
+ $on_finish->( $process->pid, $exitcode, $stdout, $stderr );
+ },
+ );
+
+ $self->add( $process );
+
+ return $process->pid;
+}
+
+=head2 $loop->resolver
+
+Returns the internally-stored L<IO::Async::Resolver> object, used for name
+resolution operations by the C<resolve>, C<connect> and C<listen> methods.
+
+=cut
+
+sub resolver
+{
+ my $self = shift;
+
+ return $self->{resolver} ||= do {
+ require IO::Async::Resolver;
+ my $resolver = IO::Async::Resolver->new;
+ $self->add( $resolver );
+ $resolver;
+ }
+}
+
+=head2 $loop->set_resolver( $resolver )
+
+Sets the internally-stored L<IO::Async::Resolver> object. In most cases this
+method should not be required, but it may be used to provide an alternative
+resolver for special use-cases.
+
+=cut
+
+sub set_resolver
+{
+ my $self = shift;
+ my ( $resolver ) = @_;
+
+ $resolver->can( $_ ) or croak "Resolver is unsuitable as it does not implement $_"
+ for qw( resolve getaddrinfo getnameinfo );
+
+ $self->{resolver} = $resolver;
+
+ $self->add( $resolver );
+}
+
+=head2 @result = $loop->resolve( %params )->get
+
+This method performs a single name resolution operation. It uses an
+internally-stored C<IO::Async::Resolver> object. For more detail, see the
+C<resolve> method on the L<IO::Async::Resolver> class.
+
+=cut
+
+sub resolve
+{
+ my $self = shift;
+ my ( %params ) = @_;
+
+ $self->resolver->resolve( %params );
+}
+
+=head2 $handle|$socket = $loop->connect( %params )->get
+
+This method performs a non-blocking connection to a given address or set of
+addresses, returning a L<IO::Async::Future> which represents the operation. On
+completion, the future will yield the connected socket handle, or the given
+L<IO::Async::Handle> object.
+
+There are two modes of operation. Firstly, a list of addresses can be provided
+which will be tried in turn. Alternatively as a convenience, if a host and
+service name are provided instead of a list of addresses, these will be
+resolved using the underlying loop's C<resolve> method into the list of
+addresses.
+
+When attempting to connect to any among a list of addresses, there may be
+failures among the first attempts, before a valid connection is made. For
+example, the resolver may have returned some IPv6 addresses, but only IPv4
+routes are valid on the system. In this case, the first C<connect(2)> syscall
+will fail. This isn't yet a fatal error, if there are more addresses to try,
+perhaps some IPv4 ones.
+
+For this reason, it is possible that the operation eventually succeeds even
+though some system calls initially fail. To be aware of individual failures,
+the optional C<on_fail> callback can be used. This will be invoked on each
+individual C<socket(2)> or C<connect(2)> failure, which may be useful for
+debugging or logging.
+
+Because this module simply uses the C<getaddrinfo> resolver, it will be fully
+IPv6-aware if the underlying platform's resolver is. This allows programs to
+be fully IPv6-capable.
+
+In plain address mode, the C<%params> hash takes the following keys:
+
+=over 8
+
+=item addrs => ARRAY
+
+Reference to an array of (possibly-multiple) address structures to attempt to
+connect to. Each should be in the layout described for C<addr>. Such a layout
+is returned by the C<getaddrinfo> named resolver.
+
+=item addr => HASH or ARRAY
+
+Shortcut for passing a single address to connect to; it may be passed directly
+with this key, instead of in another array on its own. This should be in a
+format recognised by L<IO::Async::OS>'s C<extract_addrinfo> method.
+
+This example shows how to use the C<Socket> functions to construct one for TCP
+port 8001 on address 10.0.0.1:
+
+ $loop->connect(
+ addr => {
+ family => "inet",
+ socktype => "stream",
+ port => 8001,
+ ip => "10.0.0.1",
+ },
+ ...
+ );
+
+This example shows another way to connect to a UNIX socket at F<echo.sock>.
+
+ $loop->connect(
+ addr => {
+ family => "unix",
+ socktype => "stream",
+ path => "echo.sock",
+ },
+ ...
+ );
+
+=item local_addrs => ARRAY
+
+=item local_addr => HASH or ARRAY
+
+Optional. Similar to the C<addrs> or C<addr> parameters, these specify a local
+address or set of addresses to C<bind(2)> the socket to before
+C<connect(2)>ing it.
+
+=back
+
+When performing the resolution step too, the C<addrs> or C<addr> keys are
+ignored, and instead the following keys are taken:
+
+=over 8
+
+=item host => STRING
+
+=item service => STRING
+
+The hostname and service name to connect to.
+
+=item local_host => STRING
+
+=item local_service => STRING
+
+Optional. The hostname and/or service name to C<bind(2)> the socket to locally
+before connecting to the peer.
+
+=item family => INT
+
+=item socktype => INT
+
+=item protocol => INT
+
+=item flags => INT
+
+Optional. Other arguments to pass along with C<host> and C<service> to the
+C<getaddrinfo> call.
+
+=item socktype => STRING
+
+Optionally may instead be one of the values C<'stream'>, C<'dgram'> or
+C<'raw'> to stand for C<SOCK_STREAM>, C<SOCK_DGRAM> or C<SOCK_RAW>. This
+utility is provided to allow the caller to avoid a separate C<use Socket> only
+for importing these constants.
+
+=back
+
+It is necessary to pass the C<socktype> hint to the resolver when resolving
+the host/service names into an address, as some OS's C<getaddrinfo> functions
+require this hint. A warning is emitted if neither C<socktype> nor C<protocol>
+hint is defined when performing a C<getaddrinfo> lookup. To avoid this warning
+while still specifying no particular C<socktype> hint (perhaps to invoke some
+OS-specific behaviour), pass C<0> as the C<socktype> value.
+
+In either case, it also accepts the following arguments:
+
+=over 8
+
+=item handle => IO::Async::Handle
+
+Optional. If given a L<IO::Async::Handle> object or a subclass (such as
+L<IO::Async::Stream> or L<IO::Async::Socket> its handle will be set to the
+newly-connected socket on success, and that handle used as the result of the
+future instead.
+
+=item on_fail => CODE
+
+Optional. After an individual C<socket(2)> or C<connect(2)> syscall has failed,
+this callback is invoked to inform of the error. It is passed the name of the
+syscall that failed, the arguments that were passed to it, and the error it
+generated. I.e.
+
+ $on_fail->( "socket", $family, $socktype, $protocol, $! );
+
+ $on_fail->( "bind", $sock, $address, $! );
+
+ $on_fail->( "connect", $sock, $address, $! );
+
+Because of the "try all" nature when given a list of multiple addresses, this
+callback may be invoked multiple times, even before an eventual success.
+
+=back
+
+This method accepts an C<extensions> parameter; see the C<EXTENSIONS> section
+below.
+
+=head2 $loop->connect( %params )
+
+When not returning a future, additional parameters can be given containing the
+continuations to invoke on success or failure.
+
+=over 8
+
+=item on_connected => CODE
+
+A continuation that is invoked on a successful C<connect(2)> call to a valid
+socket. It will be passed the connected socket handle, as an C<IO::Socket>
+object.
+
+ $on_connected->( $handle )
+
+=item on_stream => CODE
+
+An alternative to C<on_connected>, a continuation that is passed an instance
+of L<IO::Async::Stream> when the socket is connected. This is provided as a
+convenience for the common case that a Stream object is required as the
+transport for a Protocol object.
+
+ $on_stream->( $stream )
+
+=item on_socket => CODE
+
+Similar to C<on_stream>, but constructs an instance of L<IO::Async::Socket>.
+This is most useful for C<SOCK_DGRAM> or C<SOCK_RAW> sockets.
+
+ $on_socket->( $socket )
+
+=item on_connect_error => CODE
+
+A continuation that is invoked after all of the addresses have been tried, and
+none of them succeeded. It will be passed the most significant error that
+occurred, and the name of the operation it occurred in. Errors from the
+C<connect(2)> syscall are considered most significant, then C<bind(2)>, then
+finally C<socket(2)>.
+
+ $on_connect_error->( $syscall, $! )
+
+=item on_resolve_error => CODE
+
+A continuation that is invoked when the name resolution attempt fails. This is
+invoked in the same way as the C<on_error> continuation for the C<resolve>
+method.
+
+=back
+
+=cut
+
+sub connect
+{
+ my $self = shift;
+ my ( %params ) = @_;
+
+ my $extensions;
+ if( $extensions = delete $params{extensions} and @$extensions ) {
+ my ( $ext, @others ) = @$extensions;
+
+ my $method = "${ext}_connect";
+ # TODO: Try to 'require IO::Async::$ext'
+
+ $self->can( $method ) or croak "Extension method '$method' is not available";
+
+ return $self->$method(
+ %params,
+ ( @others ? ( extensions => \@others ) : () ),
+ );
+ }
+
+ my $handle = $params{handle};
+
+ my $on_done;
+ # Legacy callbacks
+ if( my $on_connected = delete $params{on_connected} ) {
+ $on_done = $on_connected;
+ }
+ elsif( my $on_stream = delete $params{on_stream} ) {
+ defined $handle and croak "Cannot pass 'on_stream' with a handle object as well";
+
+ require IO::Async::Stream;
+ # TODO: It doesn't make sense to put a SOCK_DGRAM in an
+ # IO::Async::Stream but currently we don't detect this
+ $handle = IO::Async::Stream->new;
+ $on_done = $on_stream;
+ }
+ elsif( my $on_socket = delete $params{on_socket} ) {
+ defined $handle and croak "Cannot pass 'on_socket' with a handle object as well";
+
+ require IO::Async::Socket;
+ $handle = IO::Async::Socket->new;
+ $on_done = $on_socket;
+ }
+ elsif( !defined wantarray ) {
+ croak "Expected 'on_connected' or 'on_stream' callback or to return a Future";
+ }
+
+ my $on_connect_error;
+ if( $on_connect_error = $params{on_connect_error} ) {
+ # OK
+ }
+ elsif( !defined wantarray ) {
+ croak "Expected 'on_connect_error' callback";
+ }
+
+ my $on_resolve_error;
+ if( $on_resolve_error = $params{on_resolve_error} ) {
+ # OK
+ }
+ elsif( !defined wantarray and exists $params{host} || exists $params{local_host} ) {
+ croak "Expected 'on_resolve_error' callback or to return a Future";
+ }
+
+ my $connector = $self->{connector} ||= $self->__new_feature( "IO::Async::Internals::Connector" );
+
+ my $future = $connector->connect( %params );
+
+ $future = $future->then( sub {
+ $handle->set_handle( shift );
+ return Future->done( $handle )
+ }) if $handle;
+
+ $future->on_done( $on_done ) if $on_done;
+ $future->on_fail( sub {
+ $on_connect_error->( @_[2,3] ) if $on_connect_error and $_[1] eq "connect";
+ $on_resolve_error->( $_[2] ) if $on_resolve_error and $_[1] eq "resolve";
+ } );
+
+ return $future if defined wantarray;
+
+ # Caller is not going to keep hold of the Future, so we have to ensure it
+ # stays alive somehow
+ $future->on_ready( sub { undef $future } ); # intentional cycle
+}
+
+=head2 $listener = $loop->listen( %params )->get
+
+This method sets up a listening socket and arranges for an acceptor callback
+to be invoked each time a new connection is accepted on the socket. Internally
+it creates an instance of L<IO::Async::Listener> and adds it to the Loop if
+not given one in the arguments.
+
+Addresses may be given directly, or they may be looked up using the system's
+name resolver, or a socket handle may be given directly.
+
+If multiple addresses are given, or resolved from the service and hostname,
+then each will be attempted in turn until one succeeds.
+
+In named resolver mode, the C<%params> hash takes the following keys:
+
+=over 8
+
+=item service => STRING
+
+The service name to listen on.
+
+=item host => STRING
+
+The hostname to listen on. Optional. Will listen on all addresses if not
+supplied.
+
+=item family => INT
+
+=item socktype => INT
+
+=item protocol => INT
+
+=item flags => INT
+
+Optional. Other arguments to pass along with C<host> and C<service> to the
+C<getaddrinfo> call.
+
+=item socktype => STRING
+
+Optionally may instead be one of the values C<'stream'>, C<'dgram'> or
+C<'raw'> to stand for C<SOCK_STREAM>, C<SOCK_DGRAM> or C<SOCK_RAW>. This
+utility is provided to allow the caller to avoid a separate C<use Socket> only
+for importing these constants.
+
+=back
+
+It is necessary to pass the C<socktype> hint to the resolver when resolving
+the host/service names into an address, as some OS's C<getaddrinfo> functions
+require this hint. A warning is emitted if neither C<socktype> nor C<protocol>
+hint is defined when performing a C<getaddrinfo> lookup. To avoid this warning
+while still specifying no particular C<socktype> hint (perhaps to invoke some
+OS-specific behaviour), pass C<0> as the C<socktype> value.
+
+In plain address mode, the C<%params> hash takes the following keys:
+
+=over 8
+
+=item addrs => ARRAY
+
+Reference to an array of (possibly-multiple) address structures to attempt to
+listen on. Each should be in the layout described for C<addr>. Such a layout
+is returned by the C<getaddrinfo> named resolver.
+
+=item addr => ARRAY
+
+Shortcut for passing a single address to listen on; it may be passed directly
+with this key, instead of in another array of its own. This should be in a
+format recognised by L<IO::Async::OS>'s C<extract_addrinfo> method. See also
+the C<EXAMPLES> section.
+
+=back
+
+In direct socket handle mode, the following keys are taken:
+
+=over 8
+
+=item handle => IO
+
+The listening socket handle.
+
+=back
+
+In either case, the following keys are also taken:
+
+=over 8
+
+=item on_fail => CODE
+
+Optional. A callback that is invoked if a syscall fails while attempting to
+create a listening sockets. It is passed the name of the syscall that failed,
+the arguments that were passed to it, and the error generated. I.e.
+
+ $on_fail->( "socket", $family, $socktype, $protocol, $! );
+
+ $on_fail->( "sockopt", $sock, $optname, $optval, $! );
+
+ $on_fail->( "bind", $sock, $address, $! );
+
+ $on_fail->( "listen", $sock, $queuesize, $! );
+
+=item queuesize => INT
+
+Optional. The queue size to pass to the C<listen(2)> calls. If not supplied,
+then 3 will be given instead.
+
+=item reuseaddr => BOOL
+
+Optional. If true or not supplied then the C<SO_REUSEADDR> socket option will
+be set. To prevent this, pass a false value such as 0.
+
+=item v6only => BOOL
+
+Optional. If defined, sets or clears the C<IPV6_V6ONLY> socket option on
+C<PF_INET6> sockets. This option disables the ability of C<PF_INET6> socket to
+accept connections from C<AF_INET> addresses. Not all operating systems allow
+this option to be disabled.
+
+=back
+
+An alternative which gives more control over the listener, is to create the
+C<IO::Async::Listener> object directly and add it explicitly to the Loop.
+
+This method accepts an C<extensions> parameter; see the C<EXTENSIONS> section
+below.
+
+=head2 $loop->listen( %params )
+
+When not returning a future, additional parameters can be given containing the
+continuations to invoke on success or failure.
+
+=over 8
+
+=item on_notifier => CODE
+
+Optional. A callback that is invoked when the Listener object is ready to
+receive connections. The callback is passed the Listener object itself.
+
+ $on_notifier->( $listener )
+
+If this callback is required, it may instead be better to construct the
+Listener object directly.
+
+=item on_listen => CODE
+
+Optional. A callback that is invoked when the listening socket is ready.
+Typically this would be used in the name resolver case, in order to inspect
+the socket's sockname address, or otherwise inspect the filehandle.
+
+ $on_listen->( $socket )
+
+=item on_listen_error => CODE
+
+A continuation this is invoked after all of the addresses have been tried, and
+none of them succeeded. It will be passed the most significant error that
+occurred, and the name of the operation it occurred in. Errors from the
+C<listen(2)> syscall are considered most significant, then C<bind(2)>, then
+C<sockopt(2)>, then finally C<socket(2)>.
+
+=item on_resolve_error => CODE
+
+A continuation that is invoked when the name resolution attempt fails. This is
+invoked in the same way as the C<on_error> continuation for the C<resolve>
+method.
+
+=back
+
+=cut
+
+sub listen
+{
+ my $self = shift;
+ my ( %params ) = @_;
+
+ my $remove_on_error;
+ my $listener = $params{listener} ||= do {
+ $remove_on_error++;
+
+ require IO::Async::Listener;
+
+ # Our wrappings of these don't want $listener
+ my %listenerparams;
+ for (qw( on_accept on_stream on_socket )) {
+ next unless exists $params{$_};
+ croak "Cannot ->listen with '$_' and 'listener'" if $params{listener};
+
+ my $code = delete $params{$_};
+ $listenerparams{$_} = sub {
+ shift;
+ goto &$code;
+ };
+ }
+
+ my $listener = IO::Async::Listener->new( %listenerparams );
+ $self->add( $listener );
+ $listener
+ };
+
+ my $extensions;
+ if( $extensions = delete $params{extensions} and @$extensions ) {
+ my ( $ext, @others ) = @$extensions;
+
+ # We happen to know we break older IO::Async::SSL
+ if( $ext eq "SSL" and $IO::Async::SSL::VERSION < '0.12001' ) {
+ croak "IO::Async::SSL version too old; need at least 0.12_001; found $IO::Async::SSL::VERSION";
+ }
+
+ my $method = "${ext}_listen";
+ # TODO: Try to 'require IO::Async::$ext'
+
+ $self->can( $method ) or croak "Extension method '$method' is not available";
+
+ my $f = $self->$method(
+ %params,
+ ( @others ? ( extensions => \@others ) : () ),
+ );
+ $f->on_fail( sub { $self->remove( $listener ) } ) if $remove_on_error;
+
+ return $f;
+ }
+
+ my $on_notifier = delete $params{on_notifier}; # optional
+
+ my $on_listen_error = delete $params{on_listen_error};
+ my $on_resolve_error = delete $params{on_resolve_error};
+
+ # Shortcut
+ if( $params{addr} and not $params{addrs} ) {
+ $params{addrs} = [ delete $params{addr} ];
+ }
+
+ my $f;
+ if( my $handle = delete $params{handle} ) {
+ $f = $self->_listen_handle( $listener, $handle, %params );
+ }
+ elsif( my $addrs = delete $params{addrs} ) {
+ $on_listen_error or defined wantarray or
+ croak "Expected 'on_listen_error' or to return a Future";
+ $f = $self->_listen_addrs( $listener, $addrs, %params );
+ }
+ elsif( defined $params{service} ) {
+ $on_listen_error or defined wantarray or
+ croak "Expected 'on_listen_error' or to return a Future";
+ $on_resolve_error or defined wantarray or
+ croak "Expected 'on_resolve_error' or to return a Future";
+ $f = $self->_listen_hostservice( $listener, delete $params{host}, delete $params{service}, %params );
+ }
+ else {
+ croak "Expected either 'service' or 'addrs' or 'addr' arguments";
+ }
+
+ $f->on_done( $on_notifier ) if $on_notifier;
+ if( my $on_listen = $params{on_listen} ) {
+ $f->on_done( sub { $on_listen->( shift->read_handle ) } );
+ }
+ $f->on_fail( sub {
+ my ( $message, $how, @rest ) = @_;
+ $on_listen_error->( @rest ) if $on_listen_error and $how eq "listen";
+ $on_resolve_error->( @rest ) if $on_resolve_error and $how eq "resolve";
+ });
+ $f->on_fail( sub { $self->remove( $listener ) } ) if $remove_on_error;
+
+ return $f if defined wantarray;
+
+ # Caller is not going to keep hold of the Future, so we have to ensure it
+ # stays alive somehow
+ $f->on_ready( sub { undef $f } ); # intentional cycle
+}
+
+sub _listen_handle
+{
+ my $self = shift;
+ my ( $listener, $handle, %params ) = @_;
+
+ $listener->configure( handle => $handle );
+ return $self->new_future->done( $listener );
+}
+
+sub _listen_addrs
+{
+ my $self = shift;
+ my ( $listener, $addrs, %params ) = @_;
+
+ my $queuesize = $params{queuesize} || 3;
+
+ my $on_fail = $params{on_fail};
+ !defined $on_fail or ref $on_fail or croak "Expected 'on_fail' to be a reference";
+
+ my $reuseaddr = 1;
+ $reuseaddr = 0 if defined $params{reuseaddr} and not $params{reuseaddr};
+
+ my $v6only = $params{v6only};
+
+ my ( $listenerr, $binderr, $sockopterr, $socketerr );
+
+ foreach my $addr ( @$addrs ) {
+ my ( $family, $socktype, $proto, $address ) = IO::Async::OS->extract_addrinfo( $addr );
+
+ my $sock;
+
+ unless( $sock = IO::Async::OS->socket( $family, $socktype, $proto ) ) {
+ $socketerr = $!;
+ $on_fail->( socket => $family, $socktype, $proto, $! ) if $on_fail;
+ next;
+ }
+
+ if( $reuseaddr ) {
+ unless( $sock->sockopt( SO_REUSEADDR, 1 ) ) {
+ $sockopterr = $!;
+ $on_fail->( sockopt => $sock, SO_REUSEADDR, 1, $! ) if $on_fail;
+ next;
+ }
+ }
+
+ if( defined $v6only and $family == AF_INET6 ) {
+ unless( $sock->setsockopt( IPPROTO_IPV6, IPV6_V6ONLY, $v6only ) ) {
+ $sockopterr = $!;
+ $on_fail->( sockopt => $sock, IPV6_V6ONLY, $v6only, $! ) if $on_fail;
+ next;
+ }
+ }
+
+ unless( $sock->bind( $address ) ) {
+ $binderr = $!;
+ $on_fail->( bind => $sock, $address, $! ) if $on_fail;
+ next;
+ }
+
+ unless( $sock->listen( $queuesize ) ) {
+ $listenerr = $!;
+ $on_fail->( listen => $sock, $queuesize, $! ) if $on_fail;
+ next;
+ }
+
+ return $self->_listen_handle( $listener, $sock, %params );
+ }
+
+ my $f = $self->new_future;
+ return $f->fail( "Cannot listen() - $listenerr", listen => listen => $listenerr ) if $listenerr;
+ return $f->fail( "Cannot bind() - $binderr", listen => bind => $binderr ) if $binderr;
+ return $f->fail( "Cannot setsockopt() - $sockopterr", listen => sockopt => $sockopterr ) if $sockopterr;
+ return $f->fail( "Cannot socket() - $socketerr", listen => socket => $socketerr ) if $socketerr;
+ die 'Oops; $loop->listen failed but no error cause was found';
+}
+
+sub _listen_hostservice
+{
+ my $self = shift;
+ my ( $listener, $host, $service, %params ) = @_;
+
+ $host ||= "";
+ defined $service or $service = ""; # might be 0
+
+ my %gai_hints;
+ exists $params{$_} and $gai_hints{$_} = $params{$_} for qw( family socktype protocol flags );
+
+ defined $gai_hints{socktype} or defined $gai_hints{protocol} or
+ carp "Attempting to ->listen without either 'socktype' or 'protocol' hint is not portable";
+
+ $self->resolver->getaddrinfo(
+ host => $host,
+ service => $service,
+ passive => 1,
+ %gai_hints,
+ )->then( sub {
+ my @addrs = @_;
+ $self->_listen_addrs( $listener, \@addrs, %params );
+ });
+}
+
+=head1 OS ABSTRACTIONS
+
+Because the Magic Constructor searches for OS-specific subclasses of the Loop,
+several abstractions of OS services are provided, in case specific OSes need
+to give different implementations on that OS.
+
+=cut
+
+=head2 $signum = $loop->signame2num( $signame )
+
+Legacy wrappers around L<IO::Async::OS> functions.
+
+=cut
+
+sub signame2num { shift; IO::Async::OS->signame2num( @_ ) }
+
+=head2 $time = $loop->time
+
+Returns the current UNIX time in fractional seconds. This is currently
+equivalent to C<Time::HiRes::time> but provided here as a utility for
+programs to obtain the time current used by C<IO::Async> for its own timing
+purposes.
+
+=cut
+
+sub time
+{
+ my $self = shift;
+ return Time::HiRes::time;
+}
+
+=head2 $pid = $loop->fork( %params )
+
+This method creates a new child process to run a given code block, returning
+its process ID.
+
+=over 8
+
+=item code => CODE
+
+A block of code to execute in the child process. It will be called in scalar
+context inside an C<eval> block. The return value will be used as the
+C<exit(2)> code from the child if it returns (or 255 if it returned C<undef> or
+thows an exception).
+
+=item on_exit => CODE
+
+A optional continuation to be called when the child processes exits. It will
+be invoked in the following way:
+
+ $on_exit->( $pid, $exitcode )
+
+The second argument is passed the plain perl C<$?> value.
+
+This key is optional; if not supplied, the calling code should install a
+handler using the C<watch_child> method.
+
+=item keep_signals => BOOL
+
+Optional boolean. If missing or false, any CODE references in the C<%SIG> hash
+will be removed and restored back to C<DEFAULT> in the child process. If true,
+no adjustment of the C<%SIG> hash will be performed.
+
+=back
+
+=cut
+
+sub fork
+{
+ my $self = shift;
+ my %params = @_;
+
+ HAVE_POSIX_FORK or croak "POSIX fork() is not available";
+
+ my $code = $params{code} or croak "Expected 'code' as a CODE reference";
+
+ my $kid = fork;
+ defined $kid or croak "Cannot fork() - $!";
+
+ if( $kid == 0 ) {
+ unless( $params{keep_signals} ) {
+ foreach( keys %SIG ) {
+ next if m/^__(WARN|DIE)__$/;
+ $SIG{$_} = "DEFAULT" if ref $SIG{$_} eq "CODE";
+ }
+ }
+
+ my $exitvalue = eval { $code->() };
+
+ defined $exitvalue or $exitvalue = -1;
+
+ POSIX::_exit( $exitvalue );
+ }
+
+ if( defined $params{on_exit} ) {
+ $self->watch_child( $kid => $params{on_exit} );
+ }
+
+ return $kid;
+}
+
+=head2 $tid = $loop->create_thread( %params )
+
+This method creates a new (non-detached) thread to run the given code block,
+returning its thread ID.
+
+=over 8
+
+=item code => CODE
+
+A block of code to execute in the thread. It is called in the context given by
+the C<context> argument, and its return value will be available to the
+C<on_joined> callback. It is called inside an C<eval> block; if it fails the
+exception will be caught.
+
+=item context => "scalar" | "list" | "void"
+
+Optional. Gives the calling context that C<code> is invoked in. Defaults to
+C<scalar> if not supplied.
+
+=item on_joined => CODE
+
+Callback to invoke when the thread function returns or throws an exception.
+If it returned, this callback will be invoked with its result
+
+ $on_joined->( return => @result )
+
+If it threw an exception the callback is invoked with the value of C<$@>
+
+ $on_joined->( died => $! )
+
+=back
+
+=cut
+
+# It is basically impossible to have any semblance of order on global
+# destruction, and even harder again to rely on when threads are going to be
+# terminated and joined. Instead of ensuring we join them all, just detach any
+# we no longer care about at END time
+my %threads_to_detach; # {$tid} = $thread_weakly
+END {
+ $_ and $_->detach for values %threads_to_detach;
+}
+
+sub create_thread
+{
+ my $self = shift;
+ my %params = @_;
+
+ HAVE_THREADS or croak "Threads are not available";
+
+ eval { require threads } or croak "This Perl does not support threads";
+
+ my $code = $params{code} or croak "Expected 'code' as a CODE reference";
+ my $on_joined = $params{on_joined} or croak "Expected 'on_joined' as a CODE reference";
+
+ my $threadwatches = $self->{threadwatches};
+
+ unless( $self->{thread_join_pipe} ) {
+ ( my $rd, $self->{thread_join_pipe} ) = IO::Async::OS->pipepair or
+ croak "Cannot pipepair - $!";
+ $self->{thread_join_pipe}->autoflush(1);
+
+ $self->watch_io(
+ handle => $rd,
+ on_read_ready => sub {
+ sysread $rd, my $buffer, 8192 or return;
+
+ # There's a race condition here in that we might have read from
+ # the pipe after the returning thread has written to it but before
+ # it has returned. We'll grab the actual $thread object and
+ # forcibly ->join it here to ensure we wait for its result.
+
+ foreach my $tid ( unpack "N*", $buffer ) {
+ my ( $thread, $on_joined ) = @{ delete $threadwatches->{$tid} }
+ or die "ARGH: Can't find threadwatch for tid $tid\n";
+ $on_joined->( $thread->join );
+ delete $threads_to_detach{$tid};
+ }
+ }
+ );
+ }
+
+ my $wr = $self->{thread_join_pipe};
+
+ my $context = $params{context} || "scalar";
+
+ my ( $thread ) = threads->create(
+ sub {
+ my ( @ret, $died );
+ eval {
+ $context eq "list" ? ( @ret = $code->() ) :
+ $context eq "scalar" ? ( $ret[0] = $code->() ) :
+ $code->();
+ 1;
+ } or $died = $@;
+
+ $wr->syswrite( pack "N", threads->tid );
+
+ return died => $died if $died;
+ return return => @ret;
+ }
+ );
+
+ $threadwatches->{$thread->tid} = [ $thread, $on_joined ];
+ weaken( $threads_to_detach{$thread->tid} = $thread );
+
+ return $thread->tid;
+}
+
+=head1 LOW-LEVEL METHODS
+
+As C<IO::Async::Loop> is an abstract base class, specific subclasses of it are
+required to implement certain methods that form the base level of
+functionality. They are not recommended for applications to use; see instead
+the various event objects or higher level methods listed above.
+
+These methods should be considered as part of the interface contract required
+to implement a C<IO::Async::Loop> subclass.
+
+=cut
+
+=head2 IO::Async::Loop->API_VERSION
+
+This method will be called by the magic constructor on the class before it is
+constructed, to ensure that the specific implementation will support the
+required API. This method should return the API version that the loop
+implementation supports. The magic constructor will use that class, provided
+it declares a version at least as new as the version documented here.
+
+The current API version is C<0.49>.
+
+This method may be implemented using C<constant>; e.g
+
+ use constant API_VERSION => '0.49';
+
+=cut
+
+=head2 $loop->watch_io( %params )
+
+This method installs callback functions which will be invoked when the given
+IO handle becomes read- or write-ready.
+
+The C<%params> hash takes the following keys:
+
+=over 8
+
+=item handle => IO
+
+The IO handle to watch.
+
+=item on_read_ready => CODE
+
+Optional. A CODE reference to call when the handle becomes read-ready.
+
+=item on_write_ready => CODE
+
+Optional. A CODE reference to call when the handle becomes write-ready.
+
+=back
+
+There can only be one filehandle of any given fileno registered at any one
+time. For any one filehandle, there can only be one read-readiness and/or one
+write-readiness callback at any one time. Registering a new one will remove an
+existing one of that type. It is not required that both are provided.
+
+Applications should use a C<IO::Async::Handle> or C<IO::Async::Stream> instead
+of using this method.
+
+If the filehandle does not yet have the C<O_NONBLOCK> flag set, it will be
+enabled by this method. This will ensure that any subsequent C<sysread>,
+C<syswrite>, or similar will not block on the filehandle.
+
+=cut
+
+# This class specifically does NOT implement this method, so that subclasses
+# are forced to. The constructor will be checking....
+sub __watch_io
+{
+ my $self = shift;
+ my %params = @_;
+
+ my $handle = delete $params{handle} or croak "Expected 'handle'";
+ defined eval { $handle->fileno } or croak "Expected that 'handle' has defined ->fileno";
+
+ # Silent "upgrade" to O_NONBLOCK
+ $handle->blocking and $handle->blocking(0);
+
+ my $watch = ( $self->{iowatches}->{$handle->fileno} ||= [] );
+
+ $watch->[0] = $handle;
+
+ if( exists $params{on_read_ready} ) {
+ $watch->[1] = delete $params{on_read_ready};
+ }
+
+ if( exists $params{on_write_ready} ) {
+ $watch->[2] = delete $params{on_write_ready};
+ }
+
+ if( exists $params{on_hangup} ) {
+ $self->_CAN_ON_HANGUP or croak "Cannot watch_io for 'on_hangup' in ".ref($self);
+ $watch->[3] = delete $params{on_hangup};
+ }
+
+ keys %params and croak "Unrecognised keys for ->watch_io - " . join( ", ", keys %params );
+}
+
+=head2 $loop->unwatch_io( %params )
+
+This method removes a watch on an IO handle which was previously installed by
+C<watch_io>.
+
+The C<%params> hash takes the following keys:
+
+=over 8
+
+=item handle => IO
+
+The IO handle to remove the watch for.
+
+=item on_read_ready => BOOL
+
+If true, remove the watch for read-readiness.
+
+=item on_write_ready => BOOL
+
+If true, remove the watch for write-readiness.
+
+=back
+
+Either or both callbacks may be removed at once. It is not an error to attempt
+to remove a callback that is not present. If both callbacks were provided to
+the C<watch_io> method and only one is removed by this method, the other shall
+remain.
+
+=cut
+
+sub __unwatch_io
+{
+ my $self = shift;
+ my %params = @_;
+
+ my $handle = delete $params{handle} or croak "Expected 'handle'";
+
+ my $watch = $self->{iowatches}->{$handle->fileno} or return;
+
+ if( delete $params{on_read_ready} ) {
+ undef $watch->[1];
+ }
+
+ if( delete $params{on_write_ready} ) {
+ undef $watch->[2];
+ }
+
+ if( delete $params{on_hangup} ) {
+ $self->_CAN_ON_HANGUP or croak "Cannot watch_io for 'on_hangup' in ".ref($self);
+ undef $watch->[3];
+ }
+
+ if( not $watch->[1] and not $watch->[2] and not $watch->[3] ) {
+ delete $self->{iowatches}->{$handle->fileno};
+ }
+
+ keys %params and croak "Unrecognised keys for ->unwatch_io - " . join( ", ", keys %params );
+}
+
+=head2 $loop->watch_signal( $signal, $code )
+
+This method adds a new signal handler to watch the given signal.
+
+=over 8
+
+=item $signal
+
+The name of the signal to watch to. This should be a bare name like C<TERM>.
+
+=item $code
+
+A CODE reference to the handling callback.
+
+=back
+
+There can only be one callback per signal name. Registering a new one will
+remove an existing one.
+
+Applications should use a C<IO::Async::Signal> object, or call
+C<attach_signal> instead of using this method.
+
+This and C<unwatch_signal> are optional; a subclass may implement neither, or
+both. If it implements neither then signal handling will be performed by the
+base class using a self-connected pipe to interrupt the main IO blocking.
+
+=cut
+
+sub watch_signal
+{
+ my $self = shift;
+ my ( $signal, $code ) = @_;
+
+ HAVE_SIGNALS or croak "This OS cannot ->watch_signal";
+
+ IO::Async::OS->loop_watch_signal( $self, $signal, $code );
+}
+
+=head2 $loop->unwatch_signal( $signal )
+
+This method removes the signal callback for the given signal.
+
+=over 8
+
+=item $signal
+
+The name of the signal to watch to. This should be a bare name like C<TERM>.
+
+=back
+
+=cut
+
+sub unwatch_signal
+{
+ my $self = shift;
+ my ( $signal ) = @_;
+
+ HAVE_SIGNALS or croak "This OS cannot ->unwatch_signal";
+
+ IO::Async::OS->loop_unwatch_signal( $self, $signal );
+}
+
+=head2 $id = $loop->watch_time( %args )
+
+This method installs a callback which will be called at the specified time.
+The time may either be specified as an absolute value (the C<at> key), or
+as a delay from the time it is installed (the C<after> key).
+
+The returned C<$id> value can be used to identify the timer in case it needs
+to be cancelled by the C<unwatch_time> method. Note that this value may be
+an object reference, so if it is stored, it should be released after it has
+been fired or cancelled, so the object itself can be freed.
+
+The C<%params> hash takes the following keys:
+
+=over 8
+
+=item at => NUM
+
+The absolute system timestamp to run the event.
+
+=item after => NUM
+
+The delay after now at which to run the event, if C<at> is not supplied. A
+zero or negative delayed timer should be executed as soon as possible; the
+next time the C<loop_once> method is invoked.
+
+=item now => NUM
+
+The time to consider as now if calculating an absolute time based on C<after>;
+defaults to C<time()> if not specified.
+
+=item code => CODE
+
+CODE reference to the continuation to run at the allotted time.
+
+=back
+
+Either one of C<at> or C<after> is required.
+
+For more powerful timer functionality as a C<IO::Async::Notifier> (so it can
+be used as a child within another Notifier), see instead the
+L<IO::Async::Timer> object and its subclasses.
+
+These C<*_time> methods are optional; a subclass may implement neither or both
+of them. If it implements neither, then the base class will manage a queue of
+timer events. This queue should be handled by the C<loop_once> method
+implemented by the subclass, using the C<_adjust_timeout> and
+C<_manage_queues> methods.
+
+This is the newer version of the API, replacing C<enqueue_timer>. It is
+unspecified how this method pair interacts with the older
+C<enqueue/requeue/cancel_timer> triplet.
+
+=cut
+
+sub watch_time
+{
+ my $self = shift;
+ my %args = @_;
+
+ # Renamed args
+ if( exists $args{after} ) {
+ $args{delay} = delete $args{after};
+ }
+ elsif( exists $args{at} ) {
+ $args{time} = delete $args{at};
+ }
+ else {
+ croak "Expected one of 'at' or 'after'";
+ }
+
+ if( $self->{old_timer} ) {
+ $self->enqueue_timer( %args );
+ }
+ else {
+ my $timequeue = $self->{timequeue} ||= $self->__new_feature( "IO::Async::Internals::TimeQueue" );
+
+ my $time = $self->_build_time( %args );
+ my $code = $args{code};
+
+ $timequeue->enqueue( time => $time, code => $code );
+ }
+}
+
+=head2 $loop->unwatch_time( $id )
+
+Removes a timer callback previously created by C<watch_time>.
+
+This is the newer version of the API, replacing C<cancel_timer>. It is
+unspecified how this method pair interacts with the older
+C<enqueue/requeue/cancel_timer> triplet.
+
+=cut
+
+sub unwatch_time
+{
+ my $self = shift;
+ my ( $id ) = @_;
+
+ if( $self->{old_timer} ) {
+ $self->cancel_timer( $id );
+ }
+ else {
+ my $timequeue = $self->{timequeue} ||= $self->__new_feature( "IO::Async::Internals::TimeQueue" );
+
+ $timequeue->cancel( $id );
+ }
+}
+
+sub _build_time
+{
+ my $self = shift;
+ my %params = @_;
+
+ my $time;
+ if( exists $params{time} ) {
+ $time = $params{time};
+ }
+ elsif( exists $params{delay} ) {
+ my $now = exists $params{now} ? $params{now} : $self->time;
+
+ $time = $now + $params{delay};
+ }
+ else {
+ croak "Expected either 'time' or 'delay' keys";
+ }
+
+ return $time;
+}
+
+=head2 $id = $loop->enqueue_timer( %params )
+
+An older version of C<watch_time>. This method should not be used in new code
+but is retained for legacy purposes. For simple watch/unwatch behaviour use
+instead the new C<watch_time> method; though note it has differently-named
+arguments. For requeueable timers, consider using an
+L<IO::Async::Timer::Countdown> or L<IO::Async::Timer::Absolute> instead.
+
+=cut
+
+sub enqueue_timer
+{
+ my $self = shift;
+ my ( %params ) = @_;
+
+ # Renamed args
+ $params{after} = delete $params{delay} if exists $params{delay};
+ $params{at} = delete $params{time} if exists $params{time};
+
+ my $code = $params{code};
+ return [ $self->watch_time( %params ), $code ];
+}
+
+=head2 $loop->cancel_timer( $id )
+
+An older version of C<unwatch_time>. This method should not be used in new
+code but is retained for legacy purposes.
+
+=cut
+
+sub cancel_timer
+{
+ my $self = shift;
+ my ( $id ) = @_;
+ $self->unwatch_time( $id->[0] );
+}
+
+=head2 $newid = $loop->requeue_timer( $id, %params )
+
+Reschedule an existing timer, moving it to a new time. The old timer is
+removed and will not be invoked.
+
+The C<%params> hash takes the same keys as C<enqueue_timer>, except for the
+C<code> argument.
+
+The requeue operation may be implemented as a cancel + enqueue, which may
+mean the ID changes. Be sure to store the returned C<$newid> value if it is
+required.
+
+This method should not be used in new code but is retained for legacy
+purposes. For requeueable, consider using an L<IO::Async::Timer::Countdown> or
+L<IO::Async::Timer::Absolute> instead.
+
+=cut
+
+sub requeue_timer
+{
+ my $self = shift;
+ my ( $id, %params ) = @_;
+
+ $self->unwatch_time( $id->[0] );
+ return $self->enqueue_timer( %params, code => $id->[1] );
+}
+
+=head2 $id = $loop->watch_idle( %params )
+
+This method installs a callback which will be called at some point in the near
+future.
+
+The C<%params> hash takes the following keys:
+
+=over 8
+
+=item when => STRING
+
+Specifies the time at which the callback will be invoked. See below.
+
+=item code => CODE
+
+CODE reference to the continuation to run at the allotted time.
+
+=back
+
+The C<when> parameter defines the time at which the callback will later be
+invoked. Must be one of the following values:
+
+=over 8
+
+=item later
+
+Callback is invoked after the current round of IO events have been processed
+by the loop's underlying C<loop_once> method.
+
+If a new idle watch is installed from within a C<later> callback, the
+installed one will not be invoked during this round. It will be deferred for
+the next time C<loop_once> is called, after any IO events have been handled.
+
+=back
+
+If there are pending idle handlers, then the C<loop_once> method will use a
+zero timeout; it will return immediately, having processed any IO events and
+idle handlers.
+
+The returned C<$id> value can be used to identify the idle handler in case it
+needs to be removed, by calling the C<unwatch_idle> method. Note this value
+may be a reference, so if it is stored it should be released after the
+callback has been invoked or cancled, so the referrant itself can be freed.
+
+This and C<unwatch_idle> are optional; a subclass may implement neither, or
+both. If it implements neither then idle handling will be performed by the
+base class, using the C<_adjust_timeout> and C<_manage_queues> methods.
+
+=cut
+
+sub watch_idle
+{
+ my $self = shift;
+ my %params = @_;
+
+ my $code = delete $params{code};
+ ref $code or croak "Expected 'code' to be a reference";
+
+ my $when = delete $params{when} or croak "Expected 'when'";
+
+ # Future-proofing for other idle modes
+ $when eq "later" or croak "Expected 'when' to be 'later'";
+
+ my $deferrals = $self->{deferrals};
+
+ push @$deferrals, $code;
+ return \$deferrals->[-1];
+}
+
+=head2 $loop->unwatch_idle( $id )
+
+Cancels a previously-installed idle handler.
+
+=cut
+
+sub unwatch_idle
+{
+ my $self = shift;
+ my ( $id ) = @_;
+
+ my $deferrals = $self->{deferrals};
+
+ my $idx;
+ \$deferrals->[$_] == $id and ( $idx = $_ ), last for 0 .. $#$deferrals;
+
+ splice @$deferrals, $idx, 1, () if defined $idx;
+}
+
+sub _reap_children
+{
+ my ( $childwatches ) = @_;
+
+ while( 1 ) {
+ my $zid = waitpid( -1, WNOHANG );
+
+ # PIDs on MSWin32 can be negative
+ last if !defined $zid or $zid == 0 or $zid == -1;
+ my $status = $?;
+
+ if( defined $childwatches->{$zid} ) {
+ $childwatches->{$zid}->( $zid, $status );
+ delete $childwatches->{$zid};
+ }
+
+ if( defined $childwatches->{0} ) {
+ $childwatches->{0}->( $zid, $status );
+ # Don't delete it
+ }
+ }
+}
+
+=head2 $loop->watch_child( $pid, $code )
+
+This method adds a new handler for the termination of the given child process
+PID, or all child processes.
+
+=over 8
+
+=item $pid
+
+The PID to watch. Will report on all child processes if this is 0.
+
+=item $code
+
+A CODE reference to the exit handler. It will be invoked as
+
+ $code->( $pid, $? )
+
+The second argument is passed the plain perl C<$?> value.
+
+=back
+
+After invocation, the handler for a PID-specific watch is automatically
+removed. The all-child watch will remain until it is removed by
+C<unwatch_child>.
+
+This and C<unwatch_child> are optional; a subclass may implement neither, or
+both. If it implements neither then child watching will be performed by using
+C<watch_signal> to install a C<SIGCHLD> handler, which will use C<waitpid> to
+look for exited child processes.
+
+If both a PID-specific and an all-process watch are installed, there is no
+ordering guarantee as to which will be called first.
+
+=cut
+
+sub watch_child
+{
+ my $self = shift;
+ my ( $pid, $code ) = @_;
+
+ my $childwatches = $self->{childwatches};
+
+ croak "Already have a handler for $pid" if exists $childwatches->{$pid};
+
+ if( HAVE_SIGNALS and !$self->{childwatch_sigid} ) {
+ $self->{childwatch_sigid} = $self->attach_signal(
+ CHLD => sub { _reap_children( $childwatches ) }
+ );
+
+ # There's a chance the child has already exited
+ my $zid = waitpid( $pid, WNOHANG );
+ if( defined $zid and $zid > 0 ) {
+ my $exitstatus = $?;
+ $self->later( sub { $code->( $pid, $exitstatus ) } );
+ return;
+ }
+ }
+
+ $childwatches->{$pid} = $code;
+}
+
+=head2 $loop->unwatch_child( $pid )
+
+This method removes a watch on an existing child process PID.
+
+=cut
+
+sub unwatch_child
+{
+ my $self = shift;
+ my ( $pid ) = @_;
+
+ my $childwatches = $self->{childwatches};
+
+ delete $childwatches->{$pid};
+
+ if( HAVE_SIGNALS and !keys %$childwatches ) {
+ $self->detach_signal( CHLD => delete $self->{childwatch_sigid} );
+ }
+}
+
+=head1 METHODS FOR SUBCLASSES
+
+The following methods are provided to access internal features which are
+required by specific subclasses to implement the loop functionality. The use
+cases of each will be documented in the above section.
+
+=cut
+
+=head2 $loop->_adjust_timeout( \$timeout )
+
+Shortens the timeout value passed in the scalar reference if it is longer in
+seconds than the time until the next queued event on the timer queue. If there
+are pending idle handlers, the timeout is reduced to zero.
+
+=cut
+
+sub _adjust_timeout
+{
+ my $self = shift;
+ my ( $timeref, %params ) = @_;
+
+ $$timeref = 0, return if @{ $self->{deferrals} };
+
+ if( defined $self->{sigproxy} and !$params{no_sigwait} ) {
+ $$timeref = $MAX_SIGWAIT_TIME if !defined $$timeref or $$timeref > $MAX_SIGWAIT_TIME;
+ }
+ if( !HAVE_SIGNALS and keys %{ $self->{childwatches} } ) {
+ $$timeref = $MAX_CHILDWAIT_TIME if !defined $$timeref or $$timeref > $MAX_CHILDWAIT_TIME;
+ }
+
+ my $timequeue = $self->{timequeue};
+ return unless defined $timequeue;
+
+ my $nexttime = $timequeue->next_time;
+ return unless defined $nexttime;
+
+ my $now = exists $params{now} ? $params{now} : $self->time;
+ my $timer_delay = $nexttime - $now;
+
+ if( $timer_delay < 0 ) {
+ $$timeref = 0;
+ }
+ elsif( !defined $$timeref or $timer_delay < $$timeref ) {
+ $$timeref = $timer_delay;
+ }
+}
+
+=head2 $loop->_manage_queues
+
+Checks the timer queue for callbacks that should have been invoked by now, and
+runs them all, removing them from the queue. It also invokes all of the
+pending idle handlers. Any new idle handlers installed by these are not
+invoked yet; they will wait for the next time this method is called.
+
+=cut
+
+sub _manage_queues
+{
+ my $self = shift;
+
+ my $count = 0;
+
+ my $timequeue = $self->{timequeue};
+ $count += $timequeue->fire if $timequeue;
+
+ my $deferrals = $self->{deferrals};
+ $self->{deferrals} = [];
+
+ foreach my $code ( @$deferrals ) {
+ $code->();
+ $count++;
+ }
+
+ my $childwatches = $self->{childwatches};
+ if( !HAVE_SIGNALS and keys %$childwatches ) {
+ _reap_children( $childwatches );
+ }
+
+ return $count;
+}
+
+=head1 EXTENSIONS
+
+An Extension is a Perl module that provides extra methods in the
+C<IO::Async::Loop> or other packages. They are intended to provide extra
+functionality that easily integrates with the rest of the code.
+
+Certain base methods take an C<extensions> parameter; an ARRAY reference
+containing a list of extension names. If such a list is passed to a method, it
+will immediately call a method whose name is that of the base method, prefixed
+by the first extension name in the list, separated by C<_>. If the
+C<extensions> list contains more extension names, it will be passed the
+remaining ones in another C<extensions> parameter.
+
+For example,
+
+ $loop->connect(
+ extensions => [qw( FOO BAR )],
+ %args
+ )
+
+will become
+
+ $loop->FOO_connect(
+ extensions => [qw( BAR )],
+ %args
+ )
+
+This is provided so that extension modules, such as L<IO::Async::SSL> can
+easily be invoked indirectly, by passing extra arguments to C<connect> methods
+or similar, without needing every module to be aware of the C<SSL> extension.
+This functionality is generic and not limited to C<SSL>; other extensions may
+also use it.
+
+The following methods take an C<extensions> parameter:
+
+ $loop->connect
+ $loop->listen
+
+If an extension C<listen> method is invoked, it will be passed a C<listener>
+parameter even if one was not provided to the original C<< $loop->listen >>
+call, and it will not receive any of the C<on_*> event callbacks. It should
+use the C<acceptor> parameter on the C<listener> object.
+
+=cut
+
+=head1 STALL WATCHDOG
+
+A well-behaved C<IO::Async> program should spend almost all of its time
+blocked on input using the underlying C<IO::Async::Loop> instance. The stall
+watchdog is an optional debugging feature to help detect CPU spinlocks and
+other bugs, where control is not returned to the loop every so often.
+
+If the watchdog is enabled and an event handler consumes more than a given
+amount of real time before returning to the event loop, it will be interrupted
+by printing a stack trace and terminating the program. The watchdog is only in
+effect while the loop itself is not blocking; it won't fail simply because the
+loop instance is waiting for input or timers.
+
+It is implemented using C<SIGALRM>, so if enabled, this signal will no longer
+be available to user code. (Though in any case, most uses of C<alarm()> and
+C<SIGALRM> are better served by one of the L<IO::Async::Timer> subclasses).
+
+The following environment variables control its behaviour.
+
+=over 4
+
+=item IO_ASYNC_WATCHDOG => BOOL
+
+Enables the stall watchdog if set to a non-zero value.
+
+=item IO_ASYNC_WATCHDOG_INTERVAL => INT
+
+Watchdog interval, in seconds, to pass to the C<alarm(2)> call. Defaults to 10
+seconds.
+
+=item IO_ASYNC_WATCHDOG_SIGABRT => BOOL
+
+If enabled, the watchdog signal handler will raise a C<SIGABRT>, which usually
+has the effect of breaking out of a running program in debuggers such as
+F<gdb>. If not set then the process is terminated by throwing an exception with
+C<die>.
+
+=back
+
+=cut
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/Loop/Poll.pm b/lib/IO/Async/Loop/Poll.pm
new file mode 100644
index 0000000..fb7bbf1
--- /dev/null
+++ b/lib/IO/Async/Loop/Poll.pm
@@ -0,0 +1,350 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2007-2015 -- leonerd@leonerd.org.uk
+
+package IO::Async::Loop::Poll;
+
+use strict;
+use warnings;
+
+our $VERSION = '0.67';
+use constant API_VERSION => '0.49';
+
+use base qw( IO::Async::Loop );
+
+use Carp;
+
+use IO::Poll qw( POLLIN POLLOUT POLLPRI POLLHUP POLLERR );
+
+use Errno qw( EINTR );
+use Fcntl qw( S_ISREG );
+
+# Only Linux, or FreeBSD 8.0 and above, are known always to be able to report
+# EOF conditions on filehandles using POLLHUP
+use constant _CAN_ON_HANGUP =>
+ ( $^O eq "linux" ) ||
+ ( $^O eq "freebsd" and do { no warnings 'numeric'; (POSIX::uname)[2] >= 8.0 } );
+
+# poll() on most platforms claims that ISREG files are always read- and
+# write-ready, but not on MSWin32. We need to fake this
+use constant FAKE_ISREG_READY => IO::Async::OS->HAVE_FAKE_ISREG_READY;
+# poll() on most platforms indicates POLLOUT when connect() fails, but not on
+# MSWin32. Have to poll also for POLLPRI in that case
+use constant POLL_CONNECT_POLLPRI => IO::Async::OS->HAVE_POLL_CONNECT_POLLPRI;
+
+use constant _CAN_WATCHDOG => 1;
+use constant WATCHDOG_ENABLE => IO::Async::Loop->WATCHDOG_ENABLE;
+
+=head1 NAME
+
+C<IO::Async::Loop::Poll> - use C<IO::Async> with C<poll(2)>
+
+=head1 SYNOPSIS
+
+Normally an instance of this class would not be directly constructed by a
+program. It may however, be useful for runinng L<IO::Async> with an existing
+program already using an C<IO::Poll> object.
+
+ use IO::Poll;
+ use IO::Async::Loop::Poll;
+
+ my $poll = IO::Poll->new;
+ my $loop = IO::Async::Loop::Poll->new( poll => $poll );
+
+ $loop->add( ... );
+
+ while(1) {
+ my $timeout = ...
+ my $ret = $poll->poll( $timeout );
+ $loop->post_poll;
+ }
+
+=head1 DESCRIPTION
+
+This subclass of C<IO::Async::Loop> uses the C<poll(2)> system call to perform
+read-ready and write-ready tests.
+
+By default, this loop will use the underlying C<poll()> system call directly,
+bypassing the usual L<IO::Poll> object wrapper around it because of a number
+of bugs and design flaws in that class; namely
+
+=over 2
+
+=item *
+
+L<https://rt.cpan.org/Ticket/Display.html?id=93107> - IO::Poll relies on
+stable stringification of IO handles
+
+=item *
+
+L<https://rt.cpan.org/Ticket/Display.html?id=25049> - IO::Poll->poll() with no
+handles always returns immediately
+
+=back
+
+However, to integrate with existing code that uses an C<IO::Poll> object, a
+C<post_poll> can be called immediately after the C<poll> method that
+C<IO::Poll> object. The appropriate mask bits are maintained on the
+C<IO::Poll> object when notifiers are added or removed from the loop, or when
+they change their C<want_*> status. The C<post_poll> method inspects the
+result bits and invokes the C<on_read_ready> or C<on_write_ready> methods on
+the notifiers.
+
+=cut
+
+=head1 CONSTRUCTOR
+
+=cut
+
+=head2 $loop = IO::Async::Loop::Poll->new( %args )
+
+This function returns a new instance of a C<IO::Async::Loop::Poll> object. It
+takes the following named arguments:
+
+=over 8
+
+=item C<poll>
+
+The C<IO::Poll> object to use for notification. Optional; if a value is not
+given, the underlying C<IO::Poll::_poll()> function is invoked directly,
+outside of the object wrapping.
+
+=back
+
+=cut
+
+sub new
+{
+ my $class = shift;
+ my ( %args ) = @_;
+
+ my $poll = delete $args{poll};
+
+ my $self = $class->__new( %args );
+
+ $self->{poll} = $poll;
+ $self->{pollmask} = {};
+
+ return $self;
+}
+
+=head1 METHODS
+
+=cut
+
+=head2 $count = $loop->post_poll
+
+This method checks the returned event list from a C<IO::Poll::poll> call,
+and calls any of the notification methods or callbacks that are appropriate.
+It returns the total number of callbacks that were invoked; that is, the
+total number of C<on_read_ready> and C<on_write_ready> callbacks for
+C<watch_io>, and C<watch_time> event callbacks.
+
+=cut
+
+sub post_poll
+{
+ my $self = shift;
+
+ my $iowatches = $self->{iowatches};
+ my $poll = $self->{poll};
+
+ my $count = 0;
+
+ alarm( IO::Async::Loop->WATCHDOG_INTERVAL ) if WATCHDOG_ENABLE;
+
+ foreach my $fd ( keys %$iowatches ) {
+ my $watch = $iowatches->{$fd} or next;
+
+ my $events = $poll ? $poll->events( $watch->[0] )
+ : $self->{pollevents}{$fd};
+ if( FAKE_ISREG_READY and $self->{fake_isreg}{$fd} ) {
+ $events |= $self->{fake_isreg}{$fd} & ( POLLIN|POLLOUT );
+ }
+
+ # We have to test separately because kernel doesn't report POLLIN when
+ # a pipe gets closed.
+ if( $events & (POLLIN|POLLHUP|POLLERR) ) {
+ $count++, $watch->[1]->() if defined $watch->[1];
+ }
+
+ if( $events & (POLLOUT|POLLPRI|POLLHUP|POLLERR) ) {
+ $count++, $watch->[2]->() if defined $watch->[2];
+ }
+
+ if( $events & (POLLHUP|POLLERR) ) {
+ $count++, $watch->[3]->() if defined $watch->[3];
+ }
+ }
+
+ # Since we have no way to know if the timeout occured, we'll have to
+ # attempt to fire any waiting timeout events anyway
+ $count += $self->_manage_queues;
+
+ alarm( 0 ) if WATCHDOG_ENABLE;
+
+ return $count;
+}
+
+=head2 $count = $loop->loop_once( $timeout )
+
+This method calls the C<poll> method on the stored C<IO::Poll> object,
+passing in the value of C<$timeout>, and then runs the C<post_poll> method
+on itself. It returns the total number of callbacks invoked by the
+C<post_poll> method, or C<undef> if the underlying C<poll> method returned
+an error.
+
+=cut
+
+sub loop_once
+{
+ my $self = shift;
+ my ( $timeout ) = @_;
+
+ $self->_adjust_timeout( \$timeout );
+
+ $timeout = 0 if FAKE_ISREG_READY and keys %{ $self->{fake_isreg} };
+
+ # Round up to nearest millisecond
+ if( $timeout ) {
+ my $mils = $timeout * 1000;
+ my $fraction = $mils - int $mils;
+ $timeout += ( 1 - $fraction ) / 1000 if $fraction;
+ }
+
+ if( my $poll = $self->{poll} ) {
+ my $pollret;
+
+ # There is a bug in IO::Poll at least version 0.07, where poll with no
+ # registered masks returns immediately, rather than waiting for a timeout
+ # This has been reported:
+ # http://rt.cpan.org/Ticket/Display.html?id=25049
+ if( $poll->handles ) {
+ $pollret = $poll->poll( $timeout );
+
+ if( ( $pollret == -1 and $! == EINTR ) or $pollret == 0
+ and defined $self->{sigproxy} ) {
+ # A signal occured and we have a sigproxy. Allow one more poll call
+ # with zero timeout. If it finds something, keep that result. If it
+ # finds nothing, keep -1
+
+ # Preserve $! whatever happens
+ local $!;
+
+ my $secondattempt = $poll->poll( 0 );
+ $pollret = $secondattempt if $secondattempt > 0;
+ }
+ }
+ else {
+ # Workaround - we'll use select to fake a millisecond-accurate sleep
+ $pollret = select( undef, undef, undef, $timeout );
+ }
+
+ return undef unless defined $pollret;
+ return $self->post_poll;
+ }
+ else {
+ my $msec = defined $timeout ? $timeout * 1000 : -1;
+ my @pollmasks = %{ $self->{pollmask} };
+
+ my $pollret = IO::Poll::_poll( $msec, @pollmasks );
+ if( $pollret == -1 and $! == EINTR or
+ $pollret == 0 and $self->{sigproxy} ) {
+ local $!;
+
+ @pollmasks = %{ $self->{pollmask} };
+ my $secondattempt = IO::Poll::_poll( $msec, @pollmasks );
+ $pollret = $secondattempt if $secondattempt > 0;
+ }
+
+ return undef unless defined $pollret;
+
+ $self->{pollevents} = { @pollmasks };
+ return $self->post_poll;
+ }
+}
+
+sub watch_io
+{
+ my $self = shift;
+ my %params = @_;
+
+ $self->__watch_io( %params );
+
+ my $poll = $self->{poll};
+
+ my $handle = $params{handle};
+ my $fileno = $handle->fileno;
+
+ my $curmask = $poll ? $poll->mask( $handle )
+ : $self->{pollmask}{$fileno};
+ $curmask ||= 0;
+
+ my $mask = $curmask;
+ $params{on_read_ready} and $mask |= POLLIN;
+ $params{on_write_ready} and $mask |= POLLOUT | (POLL_CONNECT_POLLPRI ? POLLPRI : 0);
+ $params{on_hangup} and $mask |= POLLHUP;
+
+ if( FAKE_ISREG_READY and S_ISREG +(stat $handle)[2] ) {
+ $self->{fake_isreg}{$fileno} = $mask;
+ }
+
+ return if $mask == $curmask;
+
+ if( $poll ) {
+ $poll->mask( $handle, $mask );
+ }
+ else {
+ $self->{pollmask}{$fileno} = $mask;
+ }
+}
+
+sub unwatch_io
+{
+ my $self = shift;
+ my %params = @_;
+
+ $self->__unwatch_io( %params );
+
+ my $poll = $self->{poll};
+
+ my $handle = $params{handle};
+ my $fileno = $handle->fileno;
+
+ my $curmask = $poll ? $poll->mask( $handle )
+ : $self->{pollmask}{$fileno};
+ $curmask ||= 0;
+
+ my $mask = $curmask;
+ $params{on_read_ready} and $mask &= ~POLLIN;
+ $params{on_write_ready} and $mask &= ~(POLLOUT | (POLL_CONNECT_POLLPRI ? POLLPRI : 0));
+ $params{on_hangup} and $mask &= ~POLLHUP;
+
+ if( FAKE_ISREG_READY and S_ISREG +(stat $handle)[2] ) {
+ if( $mask ) {
+ $self->{fake_isreg}{$handle->fileno} = $mask;
+ }
+ else {
+ delete $self->{fake_isreg}{$handle->fileno};
+ }
+ }
+
+ return if $mask == $curmask;
+
+ if( $poll ) {
+ $poll->mask( $handle, $mask );
+ }
+ else {
+ $mask ? ( $self->{pollmask}{$fileno} = $mask )
+ : ( delete $self->{pollmask}{$fileno} );
+ }
+}
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/Loop/Select.pm b/lib/IO/Async/Loop/Select.pm
new file mode 100644
index 0000000..0c3bd9c
--- /dev/null
+++ b/lib/IO/Async/Loop/Select.pm
@@ -0,0 +1,294 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2007-2015 -- leonerd@leonerd.org.uk
+
+package IO::Async::Loop::Select;
+
+use strict;
+use warnings;
+
+our $VERSION = '0.67';
+use constant API_VERSION => '0.49';
+
+use base qw( IO::Async::Loop );
+
+use IO::Async::OS;
+
+use Carp;
+
+# select() on most platforms claims that ISREG files are always read- and
+# write-ready, but not on MSWin32. We need to fake this
+use constant FAKE_ISREG_READY => IO::Async::OS->HAVE_FAKE_ISREG_READY;
+# select() on most platforms indicates write-ready when connect() fails, but
+# not on MSWin32. Have to pull from evec in that case
+use constant SELECT_CONNECT_EVEC => IO::Async::OS->HAVE_SELECT_CONNECT_EVEC;
+
+use constant _CAN_WATCHDOG => 1;
+use constant WATCHDOG_ENABLE => IO::Async::Loop->WATCHDOG_ENABLE;
+
+=head1 NAME
+
+C<IO::Async::Loop::Select> - use C<IO::Async> with C<select(2)>
+
+=head1 SYNOPSIS
+
+Normally an instance of this class would not be directly constructed by a
+program. It may however, be useful for runinng L<IO::Async> with an existing
+program already using a C<select> call.
+
+ use IO::Async::Loop::Select;
+
+ my $loop = IO::Async::Loop::Select->new;
+
+ $loop->add( ... );
+
+ while(1) {
+ my ( $rvec, $wvec, $evec ) = ('') x 3;
+ my $timeout;
+
+ $loop->pre_select( \$rvec, \$wvec, \$evec, \$timeout );
+ ...
+ my $ret = select( $rvec, $wvec, $evec, $timeout );
+ ...
+ $loop->post_select( $rvec, $evec, $wvec );
+ }
+
+=head1 DESCRIPTION
+
+This subclass of C<IO::Async::Loop> uses the C<select(2)> syscall to perform
+read-ready and write-ready tests.
+
+To integrate with an existing C<select>-based event loop, a pair of methods
+C<pre_select> and C<post_select> can be called immediately before and
+after a C<select> call. The relevant bits in the read-ready, write-ready and
+exceptional-state bitvectors are set by the C<pre_select> method, and tested
+by the C<post_select> method to pick which event callbacks to invoke.
+
+=cut
+
+=head1 CONSTRUCTOR
+
+=cut
+
+=head2 $loop = IO::Async::Loop::Select->new
+
+This function returns a new instance of a C<IO::Async::Loop::Select> object.
+It takes no special arguments.
+
+=cut
+
+sub new
+{
+ my $class = shift;
+
+ my $self = $class->__new( @_ );
+
+ $self->{rvec} = '';
+ $self->{wvec} = '';
+ $self->{evec} = '';
+
+ $self->{avec} = ''; # Bitvector of handles always to claim are ready
+
+ return $self;
+}
+
+=head1 METHODS
+
+=cut
+
+=head2 $loop->pre_select( \$readvec, \$writevec, \$exceptvec, \$timeout )
+
+This method prepares the bitvectors for a C<select> call, setting the bits
+that the Loop is interested in. It will also adjust the C<$timeout> value if
+appropriate, reducing it if the next event timeout the Loop requires is sooner
+than the current value.
+
+=over 8
+
+=item \$readvec
+
+=item \$writevec
+
+=item \$exceptvec
+
+Scalar references to the reading, writing and exception bitvectors
+
+=item \$timeout
+
+Scalar reference to the timeout value
+
+=back
+
+=cut
+
+sub pre_select
+{
+ my $self = shift;
+ my ( $readref, $writeref, $exceptref, $timeref ) = @_;
+
+ # BITWISE operations
+ $$readref |= $self->{rvec};
+ $$writeref |= $self->{wvec};
+ $$exceptref |= $self->{evec};
+
+ $self->_adjust_timeout( $timeref );
+
+ $$timeref = 0 if FAKE_ISREG_READY and length $self->{avec};
+
+ # Round up to nearest millisecond
+ if( $$timeref ) {
+ my $mils = $$timeref * 1000;
+ my $fraction = $mils - int $mils;
+ $$timeref += ( 1 - $fraction ) / 1000 if $fraction;
+ }
+
+ return;
+}
+
+=head2 $loop->post_select( $readvec, $writevec, $exceptvec )
+
+This method checks the returned bitvectors from a C<select> call, and calls
+any of the callbacks that are appropriate.
+
+=over 8
+
+=item $readvec
+
+=item $writevec
+
+=item $exceptvec
+
+Scalars containing the read-ready, write-ready and exception bitvectors
+
+=back
+
+=cut
+
+sub post_select
+{
+ my $self = shift;
+ my ( $readvec, $writevec, $exceptvec ) = @_;
+
+ my $iowatches = $self->{iowatches};
+
+ my $count = 0;
+
+ alarm( IO::Async::Loop->WATCHDOG_INTERVAL ) if WATCHDOG_ENABLE;
+
+ foreach my $fd ( keys %$iowatches ) {
+ my $watch = $iowatches->{$fd} or next;
+
+ my $fileno = $watch->[0]->fileno;
+
+ if( vec( $readvec, $fileno, 1 ) or
+ FAKE_ISREG_READY and vec( $self->{avec}, $fileno, 1 ) and vec( $self->{rvec}, $fileno, 1 ) ) {
+ $count++, $watch->[1]->() if defined $watch->[1];
+ }
+
+ if( vec( $writevec, $fileno, 1 ) or
+ SELECT_CONNECT_EVEC and vec( $exceptvec, $fileno, 1 ) or
+ FAKE_ISREG_READY and vec( $self->{avec}, $fileno, 1 ) and vec( $self->{wvec}, $fileno, 1 ) ) {
+ $count++, $watch->[2]->() if defined $watch->[2];
+ }
+ }
+
+ # Since we have no way to know if the timeout occured, we'll have to
+ # attempt to fire any waiting timeout events anyway
+
+ $self->_manage_queues;
+
+ alarm( 0 ) if WATCHDOG_ENABLE;
+}
+
+=head2 $count = $loop->loop_once( $timeout )
+
+This method calls the C<pre_select> method to prepare the bitvectors for a
+C<select> syscall, performs it, then calls C<post_select> to process the
+result. It returns the total number of callbacks invoked by the
+C<post_select> method, or C<undef> if the underlying C<select(2)> syscall
+returned an error.
+
+=cut
+
+sub loop_once
+{
+ my $self = shift;
+ my ( $timeout ) = @_;
+
+ my ( $rvec, $wvec, $evec ) = ('') x 3;
+
+ $self->pre_select( \$rvec, \$wvec, \$evec, \$timeout );
+
+ my $ret = select( $rvec, $wvec, $evec, $timeout );
+
+ if( $ret < 0 ) {
+ # r/w/e vec can't be trusted
+ $rvec = $wvec = $evec = '';
+ }
+
+ {
+ local $!;
+ $self->post_select( $rvec, $wvec, $evec );
+ }
+
+ return $ret;
+}
+
+sub watch_io
+{
+ my $self = shift;
+ my %params = @_;
+
+ $self->__watch_io( %params );
+
+ my $fileno = $params{handle}->fileno;
+
+ vec( $self->{rvec}, $fileno, 1 ) = 1 if $params{on_read_ready};
+ vec( $self->{wvec}, $fileno, 1 ) = 1 if $params{on_write_ready};
+
+ # MSWin32 does not indicate writeready for connect() errors, HUPs, etc
+ # but it does indicate exceptional
+ vec( $self->{evec}, $fileno, 1 ) = 1 if SELECT_CONNECT_EVEC and $params{on_write_ready};
+
+ vec( $self->{avec}, $fileno, 1 ) = 1 if FAKE_ISREG_READY and stat( $params{handle} ) and -f _;
+}
+
+sub unwatch_io
+{
+ my $self = shift;
+ my %params = @_;
+
+ $self->__unwatch_io( %params );
+
+ my $fileno = $params{handle}->fileno;
+
+ vec( $self->{rvec}, $fileno, 1 ) = 0 if $params{on_read_ready};
+ vec( $self->{wvec}, $fileno, 1 ) = 0 if $params{on_write_ready};
+
+ vec( $self->{evec}, $fileno, 1 ) = 0 if SELECT_CONNECT_EVEC and $params{on_write_ready};
+
+ vec( $self->{avec}, $fileno, 1 ) = 0 if FAKE_ISREG_READY and stat( $params{handle} ) and -f _;
+
+ # vec will grow a bit vector as needed, but never shrink it. We'll trim
+ # trailing null bytes
+ $_ =~s/\0+\z// for $self->{rvec}, $self->{wvec}, $self->{evec}, $self->{avec};
+}
+
+=head1 SEE ALSO
+
+=over 4
+
+=item *
+
+L<IO::Select> - OO interface to select system call
+
+=back
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/LoopTests.pm b/lib/IO/Async/LoopTests.pm
new file mode 100644
index 0000000..63f1257
--- /dev/null
+++ b/lib/IO/Async/LoopTests.pm
@@ -0,0 +1,833 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2009-2015 -- leonerd@leonerd.org.uk
+
+package IO::Async::LoopTests;
+
+use strict;
+use warnings;
+
+use Exporter 'import';
+our @EXPORT = qw(
+ run_tests
+);
+
+use Test::More;
+use Test::Fatal;
+use Test::Refcount;
+
+use IO::Async::Test qw();
+
+use IO::Async::OS;
+
+use IO::File;
+use Fcntl qw( SEEK_SET );
+use POSIX qw( SIGTERM );
+use Socket qw( sockaddr_family AF_UNIX );
+use Time::HiRes qw( time );
+
+our $VERSION = '0.67';
+
+# Abstract Units of Time
+use constant AUT => $ENV{TEST_QUICK_TIMERS} ? 0.1 : 1;
+
+# The loop under test. We keep it in a single lexical here, so we can use
+# is_oneref tests in the individual test suite functions
+my $loop;
+END { undef $loop }
+
+=head1 NAME
+
+C<IO::Async::LoopTests> - acceptance testing for C<IO::Async::Loop> subclasses
+
+=head1 SYNOPSIS
+
+ use IO::Async::LoopTests;
+ run_tests( 'IO::Async::Loop::Shiney', 'io' );
+
+=head1 DESCRIPTION
+
+This module contains a collection of test functions for running acceptance
+tests on L<IO::Async::Loop> subclasses. It is provided as a facility for
+authors of such subclasses to ensure that the code conforms to the Loop API
+required by C<IO::Async>.
+
+=head1 TIMING
+
+Certain tests require the use of timers or timed delays. Normally these are
+counted in units of seconds. By setting the environment variable
+C<TEST_QUICK_TIMERS> to some true value, these timers run 10 times quicker,
+being measured in units of 0.1 seconds instead. This value may be useful when
+running the tests interactively, to avoid them taking too long. The slower
+timers are preferred on automated smoke-testing machines, to help guard
+against false negatives reported simply because of scheduling delays or high
+system load while testing.
+
+ TEST_QUICK_TIMERS=1 ./Build test
+
+=cut
+
+=head1 FUNCTIONS
+
+=cut
+
+=head2 run_tests( $class, @tests )
+
+Runs a test or collection of tests against the loop subclass given. The class
+being tested is loaded by this function; the containing script does not need
+to C<require> or C<use> it first.
+
+This function runs C<Test::More::plan> to output its expected test count; the
+containing script should not do this.
+
+=cut
+
+sub run_tests
+{
+ my ( $testclass, @tests ) = @_;
+
+ my $count = 0;
+ $count += __PACKAGE__->can( "count_tests_$_" )->() + 4 for @tests;
+
+ plan tests => $count;
+
+ ( my $file = "$testclass.pm" ) =~ s{::}{/}g;
+
+ eval { require $file };
+ if( $@ ) {
+ BAIL_OUT( "Unable to load $testclass - $@" );
+ }
+
+ foreach my $test ( @tests ) {
+ $loop = $testclass->new;
+
+ isa_ok( $loop, $testclass, '$loop' );
+
+ is( IO::Async::Loop->new, $loop, 'magic constructor yields $loop' );
+
+ # Kill the reference in $ONE_TRUE_LOOP so as not to upset the refcounts
+ # and to ensure we get a new one each time
+ undef $IO::Async::Loop::ONE_TRUE_LOOP;
+
+ is_oneref( $loop, '$loop has refcount 1' );
+
+ __PACKAGE__->can( "run_tests_$test" )->();
+
+ is_oneref( $loop, '$loop has refcount 1 finally' );
+ }
+}
+
+sub wait_for(&)
+{
+ # Bounce via here so we don't upset refcount tests by having loop
+ # permanently set in IO::Async::Test
+ IO::Async::Test::testing_loop( $loop );
+
+ # Override prototype - I know what I'm doing
+ &IO::Async::Test::wait_for( @_ );
+
+ IO::Async::Test::testing_loop( undef );
+}
+
+sub time_between(&$$$)
+{
+ my ( $code, $lower, $upper, $name ) = @_;
+
+ my $start = time;
+ $code->();
+ my $took = ( time - $start ) / AUT;
+
+ cmp_ok( $took, '>=', $lower, "$name took at least $lower seconds" ) if defined $lower;
+ cmp_ok( $took, '<=', $upper * 3, "$name took no more than $upper seconds" ) if defined $upper;
+ if( $took > $upper and $took <= $upper * 3 ) {
+ diag( "$name took longer than $upper seconds - this may just be an indication of a busy testing machine rather than a bug" );
+ }
+}
+
+=head1 TEST SUITES
+
+The following test suite names exist, to be passed as a name in the C<@tests>
+argument to C<run_tests>:
+
+=cut
+
+=head2 io
+
+Tests the Loop's ability to watch filehandles for IO readiness
+
+=cut
+
+use constant count_tests_io => 18;
+sub run_tests_io
+{
+ {
+ my ( $S1, $S2 ) = IO::Async::OS->socketpair or die "Cannot create socket pair - $!";
+ $_->blocking( 0 ) for $S1, $S2;
+
+ my $readready = 0;
+ my $writeready = 0;
+ $loop->watch_io(
+ handle => $S1,
+ on_read_ready => sub { $readready = 1 },
+ );
+
+ is_oneref( $loop, '$loop has refcount 1 after watch_io on_read_ready' );
+ is( $readready, 0, '$readready still 0 before ->loop_once' );
+
+ $loop->loop_once( 0.1 );
+
+ is( $readready, 0, '$readready when idle' );
+
+ $S2->syswrite( "data\n" );
+
+ # We should still wait a little while even thought we expect to be ready
+ # immediately, because talking to ourself with 0 poll timeout is a race
+ # condition - we can still race with the kernel.
+
+ $loop->loop_once( 0.1 );
+
+ is( $readready, 1, '$readready after loop_once' );
+
+ # Ready $S1 to clear the data
+ $S1->getline; # ignore return
+
+ $loop->unwatch_io(
+ handle => $S1,
+ on_read_ready => 1,
+ );
+
+ $loop->watch_io(
+ handle => $S1,
+ on_read_ready => sub { $readready = 1 },
+ );
+
+ $readready = 0;
+ $S2->syswrite( "more data\n" );
+
+ $loop->loop_once( 0.1 );
+
+ is( $readready, 1, '$readready after ->unwatch_io/->watch_io' );
+
+ $S1->getline; # ignore return
+
+ $loop->watch_io(
+ handle => $S1,
+ on_write_ready => sub { $writeready = 1 },
+ );
+
+ is_oneref( $loop, '$loop has refcount 1 after watch_io on_write_ready' );
+
+ $loop->loop_once( 0.1 );
+
+ is( $writeready, 1, '$writeready after loop_once' );
+
+ $loop->unwatch_io(
+ handle => $S1,
+ on_write_ready => 1,
+ );
+
+ $readready = 0;
+ $loop->loop_once( 0.1 );
+
+ is( $readready, 0, '$readready before HUP' );
+
+ $S2->close;
+
+ $readready = 0;
+ $loop->loop_once( 0.1 );
+
+ is( $readready, 1, '$readready after HUP' );
+
+ $loop->unwatch_io(
+ handle => $S1,
+ on_read_ready => 1,
+ );
+ }
+
+ # HUP of pipe - can be different to sockets on some architectures
+ {
+ my ( $Prd, $Pwr ) = IO::Async::OS->pipepair or die "Cannot pipepair - $!";
+ $_->blocking( 0 ) for $Prd, $Pwr;
+
+ my $readready = 0;
+ $loop->watch_io(
+ handle => $Prd,
+ on_read_ready => sub { $readready = 1 },
+ );
+
+ $loop->loop_once( 0.1 );
+
+ is( $readready, 0, '$readready before pipe HUP' );
+
+ $Pwr->close;
+
+ $readready = 0;
+ $loop->loop_once( 0.1 );
+
+ is( $readready, 1, '$readready after pipe HUP' );
+
+ $loop->unwatch_io(
+ handle => $Prd,
+ on_read_ready => 1,
+ );
+ }
+
+ SKIP: {
+ $loop->_CAN_ON_HANGUP or skip "Loop cannot watch_io for on_hangup", 2;
+
+ SKIP: {
+ my ( $S1, $S2 ) = IO::Async::OS->socketpair or die "Cannot socketpair - $!";
+ $_->blocking( 0 ) for $S1, $S2;
+
+ sockaddr_family( $S1->sockname ) == AF_UNIX or skip "Cannot reliably detect hangup condition on non AF_UNIX sockets", 1;
+
+ my $hangup = 0;
+ $loop->watch_io(
+ handle => $S1,
+ on_hangup => sub { $hangup = 1 },
+ );
+
+ $S2->close;
+
+ $loop->loop_once( 0.1 );
+
+ is( $hangup, 1, '$hangup after socket close' );
+ }
+
+ my ( $Prd, $Pwr ) = IO::Async::OS->pipepair or die "Cannot pipepair - $!";
+ $_->blocking( 0 ) for $Prd, $Pwr;
+
+ my $hangup = 0;
+ $loop->watch_io(
+ handle => $Pwr,
+ on_hangup => sub { $hangup = 1 },
+ );
+
+ $Prd->close;
+
+ $loop->loop_once( 0.1 );
+
+ is( $hangup, 1, '$hangup after pipe close for writing' );
+ }
+
+ # Check that combined read/write handlers can cancel each other
+ {
+ my ( $S1, $S2 ) = IO::Async::OS->socketpair or die "Cannot socketpair - $!";
+ $_->blocking( 0 ) for $S1, $S2;
+
+ my $callcount = 0;
+ $loop->watch_io(
+ handle => $S1,
+ on_read_ready => sub {
+ $callcount++;
+ $loop->unwatch_io( handle => $S1, on_read_ready => 1, on_write_ready => 1 );
+ },
+ on_write_ready => sub {
+ $callcount++;
+ $loop->unwatch_io( handle => $S1, on_read_ready => 1, on_write_ready => 1 );
+ },
+ );
+
+ $S2->close;
+
+ $loop->loop_once( 0.1 );
+
+ is( $callcount, 1, 'read/write_ready can cancel each other' );
+ }
+
+ # Check that cross-connected handlers can cancel each other
+ {
+ my ( $SA1, $SA2 ) = IO::Async::OS->socketpair or die "Cannot socketpair - $!";
+ my ( $SB1, $SB2 ) = IO::Async::OS->socketpair or die "Cannot socketpair - $!";
+ $_->blocking( 0 ) for $SA1, $SA2, $SB1, $SB2;
+
+ my @handles = ( $SA1, $SB1 );
+
+ my $callcount = 0;
+ $loop->watch_io(
+ handle => $_,
+ on_write_ready => sub {
+ $callcount++;
+ $loop->unwatch_io( handle => $_, on_write_ready => 1 ) for @handles;
+ },
+ ) for @handles;
+
+ $loop->loop_once( 0.1 );
+
+ is( $callcount, 1, 'write_ready on crosslinked handles can cancel each other' );
+ }
+
+ # Check that error conditions that aren't true read/write-ability are still
+ # invoked
+ {
+ my ( $S1, $S2 ) = IO::Async::OS->socketpair( 'inet', 'dgram' ) or die "Cannot create AF_INET/SOCK_DGRAM connected pair - $!";
+ $_->blocking( 0 ) for $S1, $S2;
+ $S2->close;
+
+ my $readready = 0;
+ $loop->watch_io(
+ handle => $S1,
+ on_read_ready => sub { $readready = 1 },
+ );
+
+ $S1->syswrite( "Boo!" );
+
+ $loop->loop_once( 0.1 );
+
+ is( $readready, 1, 'exceptional socket invokes on_read_ready' );
+
+ $loop->unwatch_io(
+ handle => $S1,
+ on_read_ready => 1,
+ );
+ }
+
+ # Check that regular files still report read/writereadiness
+ {
+ my $F = IO::File->new_tmpfile or die "Cannot create temporary file - $!";
+
+ $F->print( "Here's some content\n" );
+ $F->seek( 0, SEEK_SET );
+
+ my $readready = 0;
+ my $writeready = 0;
+ $loop->watch_io(
+ handle => $F,
+ on_read_ready => sub { $readready = 1 },
+ on_write_ready => sub { $writeready = 1 },
+ );
+
+ $loop->loop_once( 0.1 );
+
+ is( $readready, 1, 'regular file is readready' );
+ is( $writeready, 1, 'regular file is writeready' );
+
+ $loop->unwatch_io(
+ handle => $F,
+ on_read_ready => 1,
+ on_write_ready => 1,
+ );
+ }
+}
+
+=head2 timer
+
+Tests the Loop's ability to handle timer events
+
+=cut
+
+use constant count_tests_timer => 21;
+sub run_tests_timer
+{
+ my $done = 0;
+ # New watch/unwatch API
+
+ cmp_ok( abs( $loop->time - time ), "<", 0.1, '$loop->time gives the current time' );
+
+ $loop->watch_time( after => 2 * AUT, code => sub { $done = 1; } );
+
+ is_oneref( $loop, '$loop has refcount 1 after watch_time' );
+
+ time_between {
+ my $now = time;
+ $loop->loop_once( 5 * AUT );
+
+ # poll might have returned just a little early, such that the TimerQueue
+ # doesn't think anything is ready yet. We need to handle that case.
+ while( !$done ) {
+ die "It should have been ready by now" if( time - $now > 5 * AUT );
+ $loop->loop_once( 0.1 * AUT );
+ }
+ } 1.5, 2.5, 'loop_once(5) while waiting for watch_time after';
+
+ $loop->watch_time( at => time + 2 * AUT, code => sub { $done = 2; } );
+
+ time_between {
+ my $now = time;
+ $loop->loop_once( 5 * AUT );
+
+ # poll might have returned just a little early, such that the TimerQueue
+ # doesn't think anything is ready yet. We need to handle that case.
+ while( !$done ) {
+ die "It should have been ready by now" if( time - $now > 5 * AUT );
+ $loop->loop_once( 0.1 * AUT );
+ }
+ } 1.5, 2.5, 'loop_once(5) while waiting for watch_time at';
+
+ my $cancelled_fired = 0;
+ my $id = $loop->watch_time( after => 1 * AUT, code => sub { $cancelled_fired = 1 } );
+ $loop->unwatch_time( $id );
+ undef $id;
+
+ $loop->loop_once( 2 * AUT );
+
+ ok( !$cancelled_fired, 'unwatched watch_time does not fire' );
+
+ $loop->watch_time( after => -1, code => sub { $done = 1 } );
+
+ $done = 0;
+
+ time_between {
+ $loop->loop_once while !$done;
+ } 0, 0.1, 'loop_once while waiting for negative interval timer';
+
+ {
+ my $done;
+
+ my $id;
+ $id = $loop->watch_time( after => 1 * AUT, code => sub {
+ $loop->unwatch_time( $id ); undef $id;
+ });
+
+ $loop->watch_time( after => 1.1 * AUT, code => sub {
+ $done++;
+ });
+
+ wait_for { $done };
+
+ is( $done, 1, 'Other timers still fire after self-cancelling one' );
+ }
+
+ # Legacy enqueue/requeue/cancel API
+ $done = 0;
+
+ $loop->enqueue_timer( delay => 2 * AUT, code => sub { $done = 1; } );
+
+ is_oneref( $loop, '$loop has refcount 1 after enqueue_timer' );
+
+ time_between {
+ my $now = time;
+ $loop->loop_once( 5 * AUT );
+
+ # poll might have returned just a little early, such that the TimerQueue
+ # doesn't think anything is ready yet. We need to handle that case.
+ while( !$done ) {
+ die "It should have been ready by now" if( time - $now > 5 * AUT );
+ $loop->loop_once( 0.1 * AUT );
+ }
+ } 1.5, 2.5, 'loop_once(5) while waiting for timer';
+
+ SKIP: {
+ skip "Unable to handle sub-second timers accurately", 3 unless $loop->_CAN_SUBSECOND_ACCURATELY;
+
+ # Check that short delays are achievable in one ->loop_once call
+ foreach my $delay ( 0.001, 0.01, 0.1 ) {
+ my $done;
+ my $count = 0;
+ my $start = time;
+
+ $loop->enqueue_timer( delay => $delay, code => sub { $done++ } );
+
+ while( !$done ) {
+ $loop->loop_once( 1 );
+ $count++;
+ last if time - $start > 5; # bailout
+ }
+
+ is( $count, 1, "One ->loop_once(1) sufficient for a single $delay second timer" );
+ }
+ }
+
+ $cancelled_fired = 0;
+ $id = $loop->enqueue_timer( delay => 1 * AUT, code => sub { $cancelled_fired = 1 } );
+ $loop->cancel_timer( $id );
+ undef $id;
+
+ $loop->loop_once( 2 * AUT );
+
+ ok( !$cancelled_fired, 'cancelled timer does not fire' );
+
+ $id = $loop->enqueue_timer( delay => 1 * AUT, code => sub { $done = 2; } );
+ $id = $loop->requeue_timer( $id, delay => 2 * AUT );
+
+ $done = 0;
+
+ time_between {
+ $loop->loop_once( 1 * AUT );
+
+ is( $done, 0, '$done still 0 so far' );
+
+ my $now = time;
+ $loop->loop_once( 5 * AUT );
+
+ # poll might have returned just a little early, such that the TimerQueue
+ # doesn't think anything is ready yet. We need to handle that case.
+ while( !$done ) {
+ die "It should have been ready by now" if( time - $now > 5 * AUT );
+ $loop->loop_once( 0.1 * AUT );
+ }
+ } 1.5, 2.5, 'requeued timer of delay 2';
+
+ is( $done, 2, '$done is 2 after requeued timer' );
+}
+
+=head2 signal
+
+Tests the Loop's ability to watch POSIX signals
+
+=cut
+
+use constant count_tests_signal => 14;
+sub run_tests_signal
+{
+ unless( IO::Async::OS->HAVE_SIGNALS ) {
+ SKIP: { skip "This OS does not have signals", 14; }
+ return;
+ }
+
+ my $caught = 0;
+
+ $loop->watch_signal( TERM => sub { $caught++ } );
+
+ is_oneref( $loop, '$loop has refcount 1 after watch_signal' );
+
+ $loop->loop_once( 0.1 );
+
+ is( $caught, 0, '$caught idling' );
+
+ kill SIGTERM, $$;
+
+ is( $caught, 0, '$caught before ->loop_once' );
+
+ $loop->loop_once( 0.1 );
+
+ is( $caught, 1, '$caught after ->loop_once' );
+
+ kill SIGTERM, $$;
+
+ is( $caught, 1, 'second raise is still deferred' );
+
+ $loop->loop_once( 0.1 );
+
+ is( $caught, 2, '$caught after second ->loop_once' );
+
+ is_oneref( $loop, '$loop has refcount 1 before unwatch_signal' );
+
+ $loop->unwatch_signal( 'TERM' );
+
+ is_oneref( $loop, '$loop has refcount 1 after unwatch_signal' );
+
+ my ( $cA, $cB );
+
+ my $idA = $loop->attach_signal( TERM => sub { $cA = 1 } );
+ my $idB = $loop->attach_signal( TERM => sub { $cB = 1 } );
+
+ is_oneref( $loop, '$loop has refcount 1 after 2 * attach_signal' );
+
+ kill SIGTERM, $$;
+
+ $loop->loop_once( 0.1 );
+
+ is( $cA, 1, '$cA after raise' );
+ is( $cB, 1, '$cB after raise' );
+
+ $loop->detach_signal( 'TERM', $idA );
+
+ undef $cA;
+ undef $cB;
+
+ kill SIGTERM, $$;
+
+ $loop->loop_once( 0.1 );
+
+ is( $cA, undef, '$cA after raise' );
+ is( $cB, 1, '$cB after raise' );
+
+ $loop->detach_signal( 'TERM', $idB );
+
+ ok( exception { $loop->attach_signal( 'this signal name does not exist', sub {} ) },
+ 'Bad signal name fails' );
+}
+
+=head2 idle
+
+Tests the Loop's support for idle handlers
+
+=cut
+
+use constant count_tests_idle => 11;
+sub run_tests_idle
+{
+ my $called = 0;
+
+ my $id = $loop->watch_idle( when => 'later', code => sub { $called = 1 } );
+
+ ok( defined $id, 'idle watcher id is defined' );
+
+ is( $called, 0, 'deferred sub not yet invoked' );
+
+ time_between { $loop->loop_once( 3 * AUT ) } undef, 1.0, 'loop_once(3) with deferred sub';
+
+ is( $called, 1, 'deferred sub called after loop_once' );
+
+ $loop->watch_idle( when => 'later', code => sub {
+ $loop->watch_idle( when => 'later', code => sub { $called = 2 } )
+ } );
+
+ $loop->loop_once( 1 );
+
+ is( $called, 1, 'inner deferral not yet invoked' );
+
+ $loop->loop_once( 1 );
+
+ is( $called, 2, 'inner deferral now invoked' );
+
+ $called = 2; # set it anyway in case previous test fails
+
+ $id = $loop->watch_idle( when => 'later', code => sub { $called = 20 } );
+
+ $loop->unwatch_idle( $id );
+
+ time_between { $loop->loop_once( 1 * AUT ) } 0.5, 1.5, 'loop_once(1) with unwatched deferral';
+
+ is( $called, 2, 'unwatched deferral not called' );
+
+ $id = $loop->watch_idle( when => 'later', code => sub { $called = 3 } );
+ my $timer_id = $loop->watch_time( after => 5, code => sub {} );
+
+ $loop->loop_once( 1 );
+
+ is( $called, 3, '$loop->later still invoked with enqueued timer' );
+
+ $loop->unwatch_time( $timer_id );
+
+ $loop->later( sub { $called = 4 } );
+
+ $loop->loop_once( 1 );
+
+ is( $called, 4, '$loop->later shortcut works' );
+}
+
+=head2 child
+
+Tests the Loop's support for watching child processes by PID
+
+=cut
+
+sub run_in_child(&)
+{
+ my $kid = fork;
+ defined $kid or die "Cannot fork() - $!";
+ return $kid if $kid;
+
+ shift->();
+ die "Fell out of run_in_child!\n";
+}
+
+use constant count_tests_child => 7;
+sub run_tests_child
+{
+ my $kid = run_in_child {
+ exit( 3 );
+ };
+
+ my $exitcode;
+
+ $loop->watch_child( $kid => sub { ( undef, $exitcode ) = @_; } );
+
+ is_oneref( $loop, '$loop has refcount 1 after watch_child' );
+ ok( !defined $exitcode, '$exitcode not defined before ->loop_once' );
+
+ undef $exitcode;
+ wait_for { defined $exitcode };
+
+ ok( ($exitcode & 0x7f) == 0, 'WIFEXITED($exitcode) after child exit' );
+ is( ($exitcode >> 8), 3, 'WEXITSTATUS($exitcode) after child exit' );
+
+ SKIP: {
+ skip "This OS does not have signals", 1 unless IO::Async::OS->HAVE_SIGNALS;
+
+ # We require that SIGTERM perform its default action; i.e. terminate the
+ # process. Ensure this definitely happens, in case the test harness has it
+ # ignored or handled elsewhere.
+ local $SIG{TERM} = "DEFAULT";
+
+ $kid = run_in_child {
+ sleep( 10 );
+ # Just in case the parent died already and didn't kill us
+ exit( 0 );
+ };
+
+ $loop->watch_child( $kid => sub { ( undef, $exitcode ) = @_; } );
+
+ kill SIGTERM, $kid;
+
+ undef $exitcode;
+ wait_for { defined $exitcode };
+
+ is( ($exitcode & 0x7f), SIGTERM, 'WTERMSIG($exitcode) after SIGTERM' );
+ }
+
+ my %kids;
+
+ $loop->watch_child( 0 => sub { my ( $kid ) = @_; delete $kids{$kid} } );
+
+ %kids = map { run_in_child { exit 0 } => 1 } 1 .. 3;
+
+ is( scalar keys %kids, 3, 'Waiting for 3 child processes' );
+
+ wait_for { !keys %kids };
+ ok( !keys %kids, 'All child processes reclaimed' );
+}
+
+=head2 control
+
+Tests that the C<run>, C<stop>, C<loop_once> and C<loop_forever> methods
+behave correctly
+
+=cut
+
+use constant count_tests_control => 8;
+sub run_tests_control
+{
+ time_between { $loop->loop_once( 0 ) } 0, 0.1, 'loop_once(0) when idle';
+
+ time_between { $loop->loop_once( 2 * AUT ) } 1.5, 2.5, 'loop_once(2) when idle';
+
+ $loop->watch_time( after => 0.1, code => sub { $loop->stop( result => "here" ) } );
+
+ local $SIG{ALRM} = sub { die "Test timed out before ->stop" };
+ alarm( 1 );
+
+ my @result = $loop->run;
+
+ alarm( 0 );
+
+ is_deeply( \@result, [ result => "here" ], '->stop arguments returned by ->run' );
+
+ $loop->watch_time( after => 0.1, code => sub { $loop->stop( result => "here" ) } );
+
+ my $result = $loop->run;
+
+ is( $result, "result", 'First ->stop argument returned by ->run in scalar context' );
+
+ $loop->watch_time( after => 0.1, code => sub {
+ $loop->watch_time( after => 0.1, code => sub { $loop->stop( "inner" ) } );
+ my @result = $loop->run;
+ $loop->stop( @result, "outer" );
+ } );
+
+ @result = $loop->run;
+
+ is_deeply( \@result, [ "inner", "outer" ], '->run can be nested properly' );
+
+ $loop->watch_time( after => 0.1, code => sub { $loop->loop_stop } );
+
+ local $SIG{ALRM} = sub { die "Test timed out before ->loop_stop" };
+ alarm( 1 );
+
+ $loop->loop_forever;
+
+ alarm( 0 );
+
+ ok( 1, '$loop->loop_forever interruptable by ->loop_stop' );
+}
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/Notifier.pm b/lib/IO/Async/Notifier.pm
new file mode 100644
index 0000000..f21c346
--- /dev/null
+++ b/lib/IO/Async/Notifier.pm
@@ -0,0 +1,919 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2006-2015 -- leonerd@leonerd.org.uk
+
+package IO::Async::Notifier;
+
+use strict;
+use warnings;
+
+our $VERSION = '0.67';
+
+use Carp;
+use Scalar::Util qw( weaken );
+
+use Future 0.26; # ->is_failed
+
+use IO::Async::Debug;
+
+# Perl 5.8.4 cannot do trampolines by modiying @_ then goto &$code
+use constant HAS_BROKEN_TRAMPOLINES => ( $] == "5.008004" );
+
+=head1 NAME
+
+C<IO::Async::Notifier> - base class for C<IO::Async> event objects
+
+=head1 SYNOPSIS
+
+Usually not directly used by a program, but one valid use case may be:
+
+ use IO::Async::Notifier;
+
+ use IO::Async::Stream;
+ use IO::Async::Signal;
+
+ use IO::Async::Loop;
+ my $loop = IO::Async::Loop->new;
+
+ my $notifier = IO::Async::Notifier->new;
+
+ $notifier->add_child(
+ IO::Async::Stream->new_for_stdin(
+ on_read => sub {
+ my $self = shift;
+ my ( $buffref, $eof ) = @_;
+
+ while( $$buffref =~ s/^(.*)\n// ) {
+ print "You said $1\n";
+ }
+
+ return 0;
+ },
+ )
+ );
+
+ $notifier->add_child(
+ IO::Async::Signal->new(
+ name => 'INT',
+ on_receipt => sub {
+ print "Goodbye!\n";
+ $loop->stop;
+ },
+ )
+ );
+
+ $loop->add( $notifier );
+
+ $loop->run;
+
+=head1 DESCRIPTION
+
+This object class forms the basis for all the other event objects that an
+C<IO::Async> program uses. It provides the lowest level of integration with a
+C<IO::Async::Loop> container, and a facility to collect Notifiers together, in
+a tree structure, where any Notifier can contain a collection of children.
+
+Normally, objects in this class would not be directly used by an end program,
+as it performs no actual IO work, and generates no actual events. These are all
+left to the various subclasses, such as:
+
+=over 4
+
+=item *
+
+L<IO::Async::Handle> - event callbacks for a non-blocking file descriptor
+
+=item *
+
+L<IO::Async::Stream> - event callbacks and write bufering for a stream
+filehandle
+
+=item *
+
+L<IO::Async::Socket> - event callbacks and send buffering for a socket
+filehandle
+
+=item *
+
+L<IO::Async::Timer> - base class for Notifiers that use timed delays
+
+=item *
+
+L<IO::Async::Signal> - event callback on receipt of a POSIX signal
+
+=item *
+
+L<IO::Async::PID> - event callback on exit of a child process
+
+=item *
+
+L<IO::Async::Process> - start and manage a child process
+
+=back
+
+For more detail, see the SYNOPSIS section in one of the above.
+
+One case where this object class would be used, is when a library wishes to
+provide a sub-component which consists of multiple other C<Notifier>
+subclasses, such as C<Handle>s and C<Timers>, but no particular object is
+suitable to be the root of a tree. In this case, a plain C<Notifier> object
+can be used as the tree root, and all the other notifiers added as children of
+it.
+
+=cut
+
+=head1 AS A MIXIN
+
+Rather than being used as a subclass this package also supports being used as
+a non-principle superclass for an object, as a mix-in. It still provides
+methods and satisfies an C<isa> test, even though the constructor is not
+directly called. This simply requires that the object be based on a normal
+blessed hash reference and include C<IO::Async::Notifier> somewhere in its
+C<@ISA> list.
+
+The methods in this class all use only keys in the hash prefixed by
+C<"IO_Async_Notifier__"> for namespace purposes.
+
+This is intended mainly for defining a subclass of some other object that is
+also an C<IO::Async::Notifier>, suitable to be added to an C<IO::Async::Loop>.
+
+ package SomeEventSource::Async;
+ use base qw( SomeEventSource IO::Async::Notifier );
+
+ sub _add_to_loop
+ {
+ my $self = shift;
+ my ( $loop ) = @_;
+
+ # Code here to set up event handling on $loop that may be required
+ }
+
+ sub _remove_from_loop
+ {
+ my $self = shift;
+ my ( $loop ) = @_;
+
+ # Code here to undo the event handling set up above
+ }
+
+Since all the methods documented here will be available, the implementation
+may wish to use the C<configure> and C<make_event_cb> or C<invoke_event>
+methods to implement its own event callbacks.
+
+=cut
+
+=head1 EVENTS
+
+The following events are invoked, either using subclass methods or CODE
+references in parameters:
+
+=head2 on_error $message, $name, @details
+
+Invoked by C<invoke_error>.
+
+=cut
+
+=head1 PARAMETERS
+
+A specific subclass of C<IO::Async::Notifier> defines named parameters that
+control its behaviour. These may be passed to the C<new> constructor, or to
+the C<configure> method. The documentation on each specific subclass will give
+details on the parameters that exist, and their uses. Some parameters may only
+support being set once at construction time, or only support being changed if
+the object is in a particular state.
+
+The following parameters are supported by all Notifiers:
+
+=over 8
+
+=item on_error => CODE
+
+CODE reference for event handler.
+
+=item notifier_name => STRING
+
+Optional string used to identify this particular Notifier. This value will be
+returned by the C<notifier_name> method.
+
+=back
+
+=cut
+
+=head1 CONSTRUCTOR
+
+=cut
+
+=head2 $notifier = IO::Async::Notifier->new( %params )
+
+This function returns a new instance of a C<IO::Async::Notifier> object with
+the given initial values of the named parameters.
+
+Up until C<IO::Async> version 0.19, this module used to implement the IO
+handle features now found in the C<IO::Async::Handle> subclass. Code that
+needs to use any of C<handle>, C<read_handle>, C<write_handle>,
+C<on_read_ready> or C<on_write_ready> should use L<IO::Async::Handle> instead.
+
+=cut
+
+sub new
+{
+ my $class = shift;
+ my %params = @_;
+
+ my $self = bless {}, $class;
+
+ $self->_init( \%params );
+
+ $self->configure( %params );
+
+ return $self;
+}
+
+=head1 METHODS
+
+=cut
+
+=head2 $notifier->configure( %params )
+
+Adjust the named parameters of the C<Notifier> as given by the C<%params>
+hash.
+
+=cut
+
+# for subclasses to override and call down to
+sub configure
+{
+ my $self = shift;
+ my %params = @_;
+
+ foreach (qw( notifier_name on_error )) {
+ $self->{"IO_Async_Notifier__$_"} = delete $params{$_} if exists $params{$_};
+ }
+
+ $self->configure_unknown( %params ) if keys %params;
+}
+
+sub configure_unknown
+{
+ my $self = shift;
+ my %params = @_;
+
+ my $class = ref $self;
+ croak "Unrecognised configuration keys for $class - " . join( " ", keys %params );
+}
+
+=head2 $loop = $notifier->loop
+
+Returns the C<IO::Async::Loop> that this Notifier is a member of.
+
+=cut
+
+sub loop
+{
+ my $self = shift;
+ return $self->{IO_Async_Notifier__loop}
+}
+
+*get_loop = \&loop;
+
+# Only called by IO::Async::Loop, not external interface
+sub __set_loop
+{
+ my $self = shift;
+ my ( $loop ) = @_;
+
+ # early exit if no change
+ return if !$loop and !$self->loop or
+ $loop and $self->loop and $loop == $self->loop;
+
+ $self->_remove_from_loop( $self->loop ) if $self->loop;
+
+ $self->{IO_Async_Notifier__loop} = $loop;
+ weaken( $self->{IO_Async_Notifier__loop} ); # To avoid a cycle
+
+ $self->_add_to_loop( $self->loop ) if $self->loop;
+}
+
+=head2 $name = $notifier->notifier_name
+
+Returns the name to identify this Notifier. If a has not been set, it will
+return the empty string. Subclasses may wish to override this behaviour to
+return some more useful information, perhaps from configured parameters.
+
+=cut
+
+sub notifier_name
+{
+ my $self = shift;
+ return $self->{IO_Async_Notifier__notifier_name} || "";
+}
+
+=head2 $f = $notifier->adopt_future( $f )
+
+Stores a reference to the L<Future> instance within the notifier itself, so
+the reference doesn't get lost. This reference will be dropped when the future
+becomes ready (either by success or failure). Additionally, if the future
+failed the notifier's C<invoke_error> method will be informed.
+
+This means that if the notifier does not provide an C<on_error> handler, nor
+is there one anywhere in the parent chain, this will be fatal to the caller of
+C<< $f->fail >>. To avoid this being fatal if the failure is handled
+elsewhere, use the C<else_done> method on the future to obtain a sequence one
+that never fails.
+
+ $notifier->adopt_future( $f->else_done() )
+
+The future itself is returned.
+
+=cut
+
+sub adopt_future
+{
+ my $self = shift;
+ my ( $f ) = @_;
+
+ my $fkey = "$f"; # stable stringification
+
+ $self->{IO_Async_Notifier__futures}{$fkey} = $f;
+
+ $f->on_ready( $self->_capture_weakself( sub {
+ my $self = shift;
+ my ( $f ) = @_;
+
+ delete $self->{IO_Async_Notifier__futures}{$fkey};
+
+ $self->invoke_error( $f->failure ) if $f->is_failed;
+ }));
+
+ return $f;
+}
+
+=head1 CHILD NOTIFIERS
+
+During the execution of a program, it may be the case that certain IO handles
+cause other handles to be created; for example, new sockets that have been
+C<accept()>ed from a listening socket. To facilitate these, a notifier may
+contain child notifier objects, that are automatically added to or removed
+from the C<IO::Async::Loop> that manages their parent.
+
+=cut
+
+=head2 $parent = $notifier->parent
+
+Returns the parent of the notifier, or C<undef> if does not have one.
+
+=cut
+
+sub parent
+{
+ my $self = shift;
+ return $self->{IO_Async_Notifier__parent};
+}
+
+=head2 @children = $notifier->children
+
+Returns a list of the child notifiers contained within this one.
+
+=cut
+
+sub children
+{
+ my $self = shift;
+ return unless $self->{IO_Async_Notifier__children};
+ return @{ $self->{IO_Async_Notifier__children} };
+}
+
+=head2 $notifier->add_child( $child )
+
+Adds a child notifier. This notifier will be added to the containing loop, if
+the parent has one. Only a notifier that does not currently have a parent and
+is not currently a member of any loop may be added as a child. If the child
+itself has grandchildren, these will be recursively added to the containing
+loop.
+
+=cut
+
+sub add_child
+{
+ my $self = shift;
+ my ( $child ) = @_;
+
+ croak "Cannot add a child that already has a parent" if defined $child->{IO_Async_Notifier__parent};
+
+ croak "Cannot add a child that is already a member of a loop" if defined $child->loop;
+
+ if( defined( my $loop = $self->loop ) ) {
+ $loop->add( $child );
+ }
+
+ push @{ $self->{IO_Async_Notifier__children} }, $child;
+ $child->{IO_Async_Notifier__parent} = $self;
+ weaken( $child->{IO_Async_Notifier__parent} );
+
+ return;
+}
+
+=head2 $notifier->remove_child( $child )
+
+Removes a child notifier. The child will be removed from the containing loop,
+if the parent has one. If the child itself has grandchildren, these will be
+recurively removed from the loop.
+
+=cut
+
+sub remove_child
+{
+ my $self = shift;
+ my ( $child ) = @_;
+
+ LOOP: {
+ my $childrenref = $self->{IO_Async_Notifier__children};
+ for my $i ( 0 .. $#$childrenref ) {
+ next unless $childrenref->[$i] == $child;
+ splice @$childrenref, $i, 1, ();
+ last LOOP;
+ }
+
+ croak "Cannot remove child from a parent that doesn't contain it";
+ }
+
+ undef $child->{IO_Async_Notifier__parent};
+
+ if( defined( my $loop = $self->loop ) ) {
+ $loop->remove( $child );
+ }
+}
+
+=head2 $notifier->remove_from_parent
+
+Removes this notifier object from its parent (either another notifier object
+or the containing loop) if it has one. If the notifier is not a child of
+another notifier nor a member of a loop, this method does nothing.
+
+=cut
+
+sub remove_from_parent
+{
+ my $self = shift;
+
+ if( my $parent = $self->parent ) {
+ $parent->remove_child( $self );
+ }
+ elsif( my $loop = $self->loop ) {
+ $loop->remove( $self );
+ }
+}
+
+=head1 SUBCLASS METHODS
+
+C<IO::Async::Notifier> is a base class provided so that specific subclasses of
+it provide more specific behaviour. The base class provides a number of
+methods that subclasses may wish to override.
+
+If a subclass implements any of these, be sure to invoke the superclass method
+at some point within the code.
+
+=cut
+
+=head2 $notifier->_init( $paramsref )
+
+This method is called by the constructor just before calling C<configure>.
+It is passed a reference to the HASH storing the constructor arguments.
+
+This method may initialise internal details of the Notifier as required,
+possibly by using parameters from the HASH. If any parameters are
+construction-only they should be C<delete>d from the hash.
+
+=cut
+
+sub _init
+{
+ # empty default
+}
+
+=head2 $notifier->configure( %params )
+
+This method is called by the constructor to set the initial values of named
+parameters, and by users of the object to adjust the values once constructed.
+
+This method should C<delete> from the C<%params> hash any keys it has dealt
+with, then pass the remaining ones to the C<SUPER::configure>. The base
+class implementation will throw an exception if there are any unrecognised
+keys remaining.
+
+=cut
+
+=head2 $notifier->configure_unknown( %params )
+
+This method is called by the base class C<configure> method, for any remaining
+parameters that are not recognised. The default implementation throws an
+exception using C<Carp> that lists the unrecognised keys. This method is
+provided to allow subclasses to override the behaviour, perhaps to store
+unrecognised keys, or to otherwise inspect the left-over arguments for some
+other purpose.
+
+=cut
+
+=head2 $notifier->_add_to_loop( $loop )
+
+This method is called when the Notifier has been added to a Loop; either
+directly, or indirectly through being a child of a Notifer already in a loop.
+
+This method may be used to perform any initial startup activity required for
+the Notifier to be fully functional but which requires a Loop to do so.
+
+=cut
+
+sub _add_to_loop
+{
+ # empty default
+}
+
+=head2 $notifier->_remove_from_loop( $loop )
+
+This method is called when the Notifier has been removed from a Loop; either
+directly, or indirectly through being a child of a Notifier removed from the
+loop.
+
+This method may be used to undo the effects of any setup that the
+C<_add_to_loop> method had originally done.
+
+=cut
+
+sub _remove_from_loop
+{
+ # empty default
+}
+
+=head1 UTILITY METHODS
+
+=cut
+
+=head2 $mref = $notifier->_capture_weakself( $code )
+
+Returns a new CODE ref which, when invoked, will invoke the originally-passed
+ref, with additionally a reference to the Notifier as its first argument. The
+Notifier reference is stored weakly in C<$mref>, so this CODE ref may be
+stored in the Notifier itself without creating a cycle.
+
+For example,
+
+ my $mref = $notifier->_capture_weakself( sub {
+ my ( $notifier, $arg ) = @_;
+ print "Notifier $notifier got argument $arg\n";
+ } );
+
+ $mref->( 123 );
+
+This is provided as a utility for Notifier subclasses to use to build a
+callback CODEref to pass to a Loop method, but which may also want to store
+the CODE ref internally for efficiency.
+
+The C<$code> argument may also be a plain string, which will be used as a
+method name; the returned CODE ref will then invoke that method on the object.
+In this case the method name is stored symbolically in the returned CODE
+reference, and dynamically dispatched each time the reference is invoked. This
+allows it to follow code reloading, dynamic replacement of class methods, or
+other similar techniques.
+
+If the C<$mref> CODE reference is being stored in some object other than the
+one it refers to, remember that since the Notifier is only weakly captured, it
+is possible that it has been destroyed by the time the code runs, and so the
+reference will be passed as C<undef>. This should be protected against by the
+code body.
+
+ $other_object->{on_event} = $notifier->_capture_weakself( sub {
+ my $notifier = shift or return;
+ my ( @event_args ) = @_;
+ ...
+ } );
+
+For stand-alone generic implementation of this behaviour, see also L<curry>
+and C<curry::weak>.
+
+=cut
+
+sub _capture_weakself
+{
+ my $self = shift;
+ my ( $code ) = @_; # actually bare method names work too
+
+ if( !ref $code ) {
+ my $class = ref $self;
+ # Don't save this coderef, or it will break dynamic method dispatch,
+ # which means code reloading, dynamic replacement, or other funky
+ # techniques stop working
+ $self->can( $code ) or
+ croak qq(Can't locate object method "$code" via package "$class");
+ }
+
+ weaken $self;
+
+ return sub {
+ my $cv = ref( $code ) ? $code : $self->can( $code );
+
+ if( HAS_BROKEN_TRAMPOLINES ) {
+ return $cv->( $self, @_ );
+ }
+ else {
+ unshift @_, $self;
+ goto &$cv;
+ }
+ };
+}
+
+=head2 $mref = $notifier->_replace_weakself( $code )
+
+Returns a new CODE ref which, when invoked, will invoke the originally-passed
+ref, with a reference to the Notifier replacing its first argument. The
+Notifier reference is stored weakly in C<$mref>, so this CODE ref may be
+stored in the Notifier itself without creating a cycle.
+
+For example,
+
+ my $mref = $notifier->_replace_weakself( sub {
+ my ( $notifier, $arg ) = @_;
+ print "Notifier $notifier got argument $arg\n";
+ } );
+
+ $mref->( $object, 123 );
+
+This is provided as a utility for Notifier subclasses to use for event
+callbacks on other objects, where the delegated object is passed in the
+function's arguments.
+
+The C<$code> argument may also be a plain string, which will be used as a
+method name; the returned CODE ref will then invoke that method on the object.
+As with C<_capture_weakself> this is stored symbolically.
+
+As with C<_capture_weakself>, care should be taken against Notifier
+destruction if the C<$mref> CODE reference is stored in some other object.
+
+=cut
+
+sub _replace_weakself
+{
+ my $self = shift;
+ my ( $code ) = @_; # actually bare method names work too
+
+ if( !ref $code ) {
+ # Don't save this coderef, see _capture_weakself for why
+ my $class = ref $self;
+ $self->can( $code ) or
+ croak qq(Can't locate object method "$code" via package "$class");
+ }
+
+ weaken $self;
+
+ return sub {
+ my $cv = ref( $code ) ? $code : $self->can( $code );
+
+ if( HAS_BROKEN_TRAMPOLINES ) {
+ return $cv->( $self, @_[1..$#_] );
+ }
+ else {
+ # Don't assign to $_[0] directly or we will change caller's first argument
+ shift @_;
+ unshift @_, $self;
+ goto &$cv;
+ }
+ };
+}
+
+=head2 $code = $notifier->can_event( $event_name )
+
+Returns a C<CODE> reference if the object can perform the given event name,
+either by a configured C<CODE> reference parameter, or by implementing a
+method. If the object is unable to handle this event, C<undef> is returned.
+
+=cut
+
+sub can_event
+{
+ my $self = shift;
+ my ( $event_name ) = @_;
+
+ return $self->{$event_name} || $self->can( $event_name );
+}
+
+=head2 $callback = $notifier->make_event_cb( $event_name )
+
+Returns a C<CODE> reference which, when invoked, will execute the given event
+handler. Event handlers may either be subclass methods, or parameters given to
+the C<new> or C<configure> method.
+
+The event handler can be passed extra arguments by giving them to the C<CODE>
+reference; the first parameter received will be a reference to the notifier
+itself. This is stored weakly in the closure, so it is safe to store the
+resulting C<CODE> reference in the object itself without causing a reference
+cycle.
+
+=cut
+
+sub make_event_cb
+{
+ my $self = shift;
+ my ( $event_name ) = @_;
+
+ my $code = $self->can_event( $event_name )
+ or croak "$self cannot handle $event_name event";
+
+ my $caller = caller;
+
+ return $self->_capture_weakself(
+ !$IO::Async::Debug::DEBUG ? $code : sub {
+ my $self = $_[0];
+ $self->_debug_printf_event( $caller, $event_name );
+ goto &$code;
+ }
+ );
+}
+
+=head2 $callback = $notifier->maybe_make_event_cb( $event_name )
+
+Similar to C<make_event_cb> but will return C<undef> if the object cannot
+handle the named event, rather than throwing an exception.
+
+=cut
+
+sub maybe_make_event_cb
+{
+ my $self = shift;
+ my ( $event_name ) = @_;
+
+ my $code = $self->can_event( $event_name )
+ or return undef;
+
+ my $caller = caller;
+
+ return $self->_capture_weakself(
+ !$IO::Async::Debug::DEBUG ? $code : sub {
+ my $self = $_[0];
+ $self->_debug_printf_event( $caller, $event_name );
+ goto &$code;
+ }
+ );
+}
+
+=head2 @ret = $notifier->invoke_event( $event_name, @args )
+
+Invokes the given event handler, passing in the given arguments. Event
+handlers may either be subclass methods, or parameters given to the C<new> or
+C<configure> method. Returns whatever the underlying method or CODE reference
+returned.
+
+=cut
+
+sub invoke_event
+{
+ my $self = shift;
+ my ( $event_name, @args ) = @_;
+
+ my $code = $self->can_event( $event_name )
+ or croak "$self cannot handle $event_name event";
+
+ $self->_debug_printf_event( scalar caller, $event_name ) if $IO::Async::Debug::DEBUG;
+ return $code->( $self, @args );
+}
+
+=head2 $retref = $notifier->maybe_invoke_event( $event_name, @args )
+
+Similar to C<invoke_event> but will return C<undef> if the object cannot
+handle the name event, rather than throwing an exception. In order to
+distinguish this from an event-handling function that simply returned
+C<undef>, if the object does handle the event, the list that it returns will
+be returned in an ARRAY reference.
+
+=cut
+
+sub maybe_invoke_event
+{
+ my $self = shift;
+ my ( $event_name, @args ) = @_;
+
+ my $code = $self->can_event( $event_name )
+ or return undef;
+
+ $self->_debug_printf_event( scalar caller, $event_name ) if $IO::Async::Debug::DEBUG;
+ return [ $code->( $self, @args ) ];
+}
+
+=head1 DEBUGGING SUPPORT
+
+=cut
+
+=head2 $notifier->debug_printf( $format, @args )
+
+Conditionally print a debugging message to C<STDERR> if debugging is enabled.
+If such a message is printed, it will be printed using C<printf> using the
+given format and arguments. The message will be prefixed with an string, in
+square brackets, to help identify the C<$notifier> instance. This string will
+be the class name of the notifier, and any parent notifiers it is contained
+by, joined by an arrow C<< <- >>. To ensure this string does not grow too
+long, certain prefixes are abbreviated:
+
+ IO::Async::Protocol:: => IaP:
+ IO::Async:: => Ia:
+ Net::Async:: => Na:
+
+Finally, each notifier that has a name defined using the C<notifier_name>
+parameter has that name appended in braces.
+
+For example, invoking
+
+ $stream->debug_printf( "EVENT on_read" )
+
+On an C<IO::Async::Stream> instance reading and writing a file descriptor
+whose C<fileno> is 4, which is a child of an C<IO::Async::Protocol::Stream>,
+will produce a line of output:
+
+ [Ia:Stream{rw=4}<-IaP:Stream] EVENT on_read
+
+=cut
+
+sub debug_printf
+{
+ $IO::Async::Debug::DEBUG or return;
+
+ my $self = shift;
+ my ( $format, @args ) = @_;
+
+ my @id;
+ while( $self ) {
+ push @id, ref $self;
+
+ my $name = $self->notifier_name;
+ $id[-1] .= "{$name}" if defined $name and length $name;
+
+ $self = $self->parent;
+ }
+
+ s/^IO::Async::Protocol::/IaP:/,
+ s/^IO::Async::/Ia:/,
+ s/^Net::Async::/Na:/ for @id;
+
+ IO::Async::Debug::logf "[%s] $format\n", join("<-", @id), @args;
+}
+
+sub _debug_printf_event
+{
+ my $self = shift;
+ my ( $caller, $event_name ) = @_;
+
+ my $class = ref $self;
+
+ if( $IO::Async::Debug::DEBUG > 1 or $class eq $caller ) {
+ s/^IO::Async::Protocol::/IaP:/,
+ s/^IO::Async::/Ia:/,
+ s/^Net::Async::/Na:/ for my $str_caller = $caller;
+
+ $self->debug_printf( "EVENT %s",
+ ( $class eq $caller ? $event_name : "${str_caller}::$event_name" )
+ );
+ }
+}
+
+=head2 $notifier->invoke_error( $message, $name, @details )
+
+Invokes the stored C<on_error> event handler, passing in the given arguments.
+If no handler is defined, it will be passed up to the containing parent
+notifier, if one exists. If no parent exists, the error message will be thrown
+as an exception by using C<die()> and this method will not return.
+
+If a handler is found to handle this error, the method will return as normal.
+However, as the expected use-case is to handle "fatal" errors that now render
+the notifier unsuitable to continue, code should be careful not to perform any
+further work after invoking it. Specifically, sockets may become disconnected,
+or the entire notifier may now be removed from its containing loop.
+
+The C<$name> and C<@details> list should follow similar semantics to L<Future>
+failures. That is, the C<$name> should be a string giving a category of
+failure, and the C<@details> list should contain additional arguments that
+relate to that kind of failure.
+
+=cut
+
+sub invoke_error
+{
+ my $self = shift;
+ my ( $message, $name, @details ) = @_;
+
+ if( my $code = $self->{IO_Async_Notifier__on_error} || $self->can( "on_error" ) ) {
+ return $code->( $self, $message, $name, @details );
+ }
+
+ if( my $parent = $self->parent ) {
+ return $parent->invoke_error( @_ );
+ }
+
+ die "$message\n";
+}
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/OS.pm b/lib/IO/Async/OS.pm
new file mode 100644
index 0000000..db16138
--- /dev/null
+++ b/lib/IO/Async/OS.pm
@@ -0,0 +1,599 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2012-2015 -- leonerd@leonerd.org.uk
+
+package IO::Async::OS;
+
+use strict;
+use warnings;
+
+our $VERSION = '0.67';
+
+our @ISA = qw( IO::Async::OS::_Base );
+
+if( eval { require "IO/Async/OS/$^O.pm" } ) {
+ @ISA = "IO::Async::OS::$^O";
+}
+
+package # hide from CPAN
+ IO::Async::OS::_Base;
+
+use Carp;
+
+use Socket 1.95 qw(
+ AF_INET AF_INET6 AF_UNIX INADDR_LOOPBACK SOCK_DGRAM SOCK_RAW SOCK_STREAM
+ pack_sockaddr_in inet_aton
+ pack_sockaddr_in6 inet_pton
+ pack_sockaddr_un
+);
+
+use IO::Socket (); # empty import
+
+use POSIX qw( sysconf _SC_OPEN_MAX );
+
+# Win32 [and maybe other places] don't have an _SC_OPEN_MAX. About the best we
+# can do really is just make up some largeish number and hope for the best.
+use constant OPEN_MAX_FD => eval { sysconf(_SC_OPEN_MAX) } || 1024;
+
+# Some constants that define features of the OS
+
+use constant HAVE_SOCKADDR_IN6 => defined eval { pack_sockaddr_in6 0, inet_pton( AF_INET6, "2001::1" ) };
+use constant HAVE_SOCKADDR_UN => defined eval { pack_sockaddr_un "/foo" };
+
+# Do we have to fake S_ISREG() files read/write-ready in select()?
+use constant HAVE_FAKE_ISREG_READY => 0;
+
+# Do we have to select() for for evec to get connect() failures
+use constant HAVE_SELECT_CONNECT_EVEC => 0;
+# Ditto; do we have to poll() for POLLPRI to get connect() failures
+use constant HAVE_POLL_CONNECT_POLLPRI => 0;
+
+# Does connect() yield EWOULDBLOCK for nonblocking in progress?
+use constant HAVE_CONNECT_EWOULDBLOCK => 0;
+
+# Can we rename() files that are open?
+use constant HAVE_RENAME_OPEN_FILES => 1;
+
+# Do we have IO::Socket::IP available?
+use constant HAVE_IO_SOCKET_IP => defined eval { require IO::Socket::IP };
+
+# Can we reliably watch for POSIX signals, including SIGCHLD to reliably
+# inform us that a fork()ed child has exit()ed?
+use constant HAVE_SIGNALS => 1;
+
+# Do we support POSIX-style true fork()ed processes at all?
+use constant HAVE_POSIX_FORK => !$ENV{IO_ASYNC_NO_FORK};
+# Can we potentially support threads? (would still need to 'require threads')
+use constant HAVE_THREADS => !$ENV{IO_ASYNC_NO_THREADS} &&
+ eval { require Config && $Config::Config{useithreads} };
+
+# Preferred trial order for built-in Loop classes
+use constant LOOP_BUILTIN_CLASSES => qw( Poll Select );
+
+# Should there be any other Loop classes we try before the builtin ones?
+use constant LOOP_PREFER_CLASSES => ();
+
+=head1 NAME
+
+C<IO::Async::OS> - operating system abstractions for C<IO::Async>
+
+=head1 DESCRIPTION
+
+This module acts as a class to provide a number of utility methods whose exact
+behaviour may depend on the type of OS it is running on. It is provided as a
+class so that specific kinds of operating system can override methods in it.
+
+As well as these support functions it also provides a number of constants, all
+with names beginning C<HAVE_> which describe various features that may or may
+not be available on the OS or perl build. Most of these are either hard-coded
+per OS, or detected at runtime.
+
+The following constants may be overridden by environment variables.
+
+=over 4
+
+=item * HAVE_POSIX_FORK
+
+True if the C<fork()> call has full POSIX semantics (full process separation).
+This is true on most OSes but false on MSWin32.
+
+This may be overridden to be false by setting the environment variable
+C<IO_ASYNC_NO_FORK>.
+
+=item * HAVE_THREADS
+
+True if C<ithreads> are available, meaning that the C<threads> module can be
+used. This depends on whether perl was built with threading support.
+
+This may be overridable to be false by setting the environment variable
+C<IO_ASYNC_NO_THREADS>.
+
+=back
+
+=cut
+
+=head2 $family = IO::Async::OS->getfamilybyname( $name )
+
+Return a protocol family value based on the given name. If C<$name> looks like
+a number it will be returned as-is. The string values C<inet>, C<inet6> and
+C<unix> will be converted to the appropriate C<AF_*> constant.
+
+=cut
+
+sub getfamilybyname
+{
+ shift;
+ my ( $name ) = @_;
+
+ return undef unless defined $name;
+
+ return $name if $name =~ m/^\d+$/;
+
+ return AF_INET if $name eq "inet";
+ return AF_INET6() if $name eq "inet6" and defined &AF_INET6;
+ return AF_UNIX if $name eq "unix";
+
+ croak "Unrecognised socktype name '$name'";
+}
+
+=head2 $socktype = IO::Async::OS->getsocktypebyname( $name )
+
+Return a socket type value based on the given name. If C<$name> looks like a
+number it will be returned as-is. The string values C<stream>, C<dgram> and
+C<raw> will be converted to the appropriate C<SOCK_*> constant.
+
+=cut
+
+sub getsocktypebyname
+{
+ shift;
+ my ( $name ) = @_;
+
+ return undef unless defined $name;
+
+ return $name if $name =~ m/^\d+$/;
+
+ return SOCK_STREAM if $name eq "stream";
+ return SOCK_DGRAM if $name eq "dgram";
+ return SOCK_RAW if $name eq "raw";
+
+ croak "Unrecognised socktype name '$name'";
+}
+
+# This one isn't documented because it's not really overridable. It's largely
+# here just for completeness
+sub socket
+{
+ my $self = shift;
+ my ( $family, $socktype, $proto ) = @_;
+
+ croak "Cannot create a new socket without a family" unless $family;
+ # PF_UNSPEC and undef are both false
+ $family = $self->getfamilybyname( $family ) || AF_UNIX;
+
+ # SOCK_STREAM is the most likely
+ $socktype = $self->getsocktypebyname( $socktype ) || SOCK_STREAM;
+
+ defined $proto or $proto = 0;
+
+ if( HAVE_IO_SOCKET_IP and ( $family == AF_INET || $family == AF_INET6() ) ) {
+ return IO::Socket::IP->new->socket( $family, $socktype, $proto );
+ }
+
+ my $sock = eval {
+ IO::Socket->new(
+ Domain => $family,
+ Type => $socktype,
+ Proto => $proto,
+ );
+ };
+ return $sock if $sock;
+
+ # That failed. Most likely because the Domain was unrecognised. This
+ # usually happens if getaddrinfo returns an AF_INET6 address but we don't
+ # have a suitable class loaded. In this case we'll return a generic one.
+ # It won't be in the specific subclass but that's the best we can do. And
+ # it will still work as a generic socket.
+ return IO::Socket->new->socket( $family, $socktype, $proto );
+}
+
+=head2 ( $S1, $S2 ) = IO::Async::OS->socketpair( $family, $socktype, $proto )
+
+An abstraction of the C<socketpair(2)> syscall, where any argument may be
+missing (or given as C<undef>).
+
+If C<$family> is not provided, a suitable value will be provided by the OS
+(likely C<AF_UNIX> on POSIX-based platforms). If C<$socktype> is not provided,
+then C<SOCK_STREAM> will be used.
+
+Additionally, this method supports building connected C<SOCK_STREAM> or
+C<SOCK_DGRAM> pairs in the C<AF_INET> family even if the underlying platform's
+C<socketpair(2)> does not, by connecting two normal sockets together.
+
+C<$family> and C<$socktype> may also be given symbolically as defined by
+C<getfamilybyname> and C<getsocktypebyname>.
+
+=cut
+
+sub socketpair
+{
+ my $self = shift;
+ my ( $family, $socktype, $proto ) = @_;
+
+ # PF_UNSPEC and undef are both false
+ $family = $self->getfamilybyname( $family ) || AF_UNIX;
+
+ # SOCK_STREAM is the most likely
+ $socktype = $self->getsocktypebyname( $socktype ) || SOCK_STREAM;
+
+ $proto ||= 0;
+
+ my ( $S1, $S2 ) = IO::Socket->new->socketpair( $family, $socktype, $proto );
+ return ( $S1, $S2 ) if defined $S1;
+
+ return unless $family == AF_INET and ( $socktype == SOCK_STREAM or $socktype == SOCK_DGRAM );
+
+ # Now lets emulate an AF_INET socketpair call
+
+ my $Stmp = IO::Async::OS->socket( $family, $socktype ) or return;
+ $Stmp->bind( pack_sockaddr_in( 0, INADDR_LOOPBACK ) ) or return;
+
+ $S1 = IO::Async::OS->socket( $family, $socktype ) or return;
+
+ if( $socktype == SOCK_STREAM ) {
+ $Stmp->listen( 1 ) or return;
+ $S1->connect( getsockname $Stmp ) or return;
+ $S2 = $Stmp->accept or return;
+
+ # There's a bug in IO::Socket here, in that $S2 's ->socktype won't
+ # yet be set. We can apply a horribly hacky fix here
+ # defined $S2->socktype and $S2->socktype == $socktype or
+ # ${*$S2}{io_socket_type} = $socktype;
+ # But for now we'll skip the test for it instead
+ }
+ else {
+ $S2 = $Stmp;
+ $S1->connect( getsockname $S2 ) or return;
+ $S2->connect( getsockname $S1 ) or return;
+ }
+
+ return ( $S1, $S2 );
+}
+
+=head2 ( $rd, $wr ) = IO::Async::OS->pipepair
+
+An abstraction of the C<pipe(2)> syscall, which returns the two new handles.
+
+=cut
+
+sub pipepair
+{
+ my $self = shift;
+
+ pipe( my ( $rd, $wr ) ) or return;
+ return ( $rd, $wr );
+}
+
+=head2 ( $rdA, $wrA, $rdB, $wrB ) = IO::Async::OS->pipequad
+
+This method is intended for creating two pairs of filehandles that are linked
+together, suitable for passing as the STDIN/STDOUT pair to a child process.
+After this function returns, C<$rdA> and C<$wrA> will be a linked pair, as
+will C<$rdB> and C<$wrB>.
+
+On platforms that support C<socketpair(2)>, this implementation will be
+preferred, in which case C<$rdA> and C<$wrB> will actually be the same
+filehandle, as will C<$rdB> and C<$wrA>. This saves a file descriptor in the
+parent process.
+
+When creating a C<IO::Async::Stream> or subclass of it, the C<read_handle>
+and C<write_handle> parameters should always be used.
+
+ my ( $childRd, $myWr, $myRd, $childWr ) = IO::Async::OS->pipequad;
+
+ IO::Async::OS->open_child(
+ stdin => $childRd,
+ stdout => $childWr,
+ ...
+ );
+
+ my $str = IO::Async::Stream->new(
+ read_handle => $myRd,
+ write_handle => $myWr,
+ ...
+ );
+ IO::Async::OS->add( $str );
+
+=cut
+
+sub pipequad
+{
+ my $self = shift;
+
+ # Prefer socketpair
+ if( my ( $S1, $S2 ) = $self->socketpair ) {
+ return ( $S1, $S2, $S2, $S1 );
+ }
+
+ # Can't do that, fallback on pipes
+ my ( $rdA, $wrA ) = $self->pipepair or return;
+ my ( $rdB, $wrB ) = $self->pipepair or return;
+
+ return ( $rdA, $wrA, $rdB, $wrB );
+}
+
+=head2 $signum = IO::Async::OS->signame2num( $signame )
+
+This utility method converts a signal name (such as "TERM") into its system-
+specific signal number. This may be useful to pass to C<POSIX::SigSet> or use
+in other places which use numbers instead of symbolic names.
+
+=cut
+
+my %sig_num;
+sub _init_signum
+{
+ my $self = shift;
+ # Copypasta from Config.pm's documentation
+
+ our %Config;
+ require Config;
+ Config->import;
+
+ unless($Config{sig_name} && $Config{sig_num}) {
+ die "No signals found";
+ }
+ else {
+ my @names = split ' ', $Config{sig_name};
+ @sig_num{@names} = split ' ', $Config{sig_num};
+ }
+}
+
+sub signame2num
+{
+ my $self = shift;
+ my ( $signame ) = @_;
+
+ %sig_num or $self->_init_signum;
+
+ return $sig_num{$signame};
+}
+
+=head2 ( $family, $socktype, $protocol, $addr ) = IO::Async::OS->extract_addrinfo( $ai )
+
+Given an ARRAY or HASH reference value containing an addrinfo, returns a
+family, socktype and protocol argument suitable for a C<socket> call and an
+address suitable for C<connect> or C<bind>.
+
+If given an ARRAY it should be in the following form:
+
+ [ $family, $socktype, $protocol, $addr ]
+
+If given a HASH it should contain the following keys:
+
+ family socktype protocol addr
+
+Each field in the result will be initialised to 0 (or empty string for the
+address) if not defined in the C<$ai> value.
+
+The family type may also be given as a symbolic string as defined by
+C<getfamilybyname>.
+
+The socktype may also be given as a symbolic string; C<stream>, C<dgram> or
+C<raw>; this will be converted to the appropriate C<SOCK_*> constant.
+
+Note that the C<addr> field, if provided, must be a packed socket address,
+such as returned by C<pack_sockaddr_in> or C<pack_sockaddr_un>.
+
+If the HASH form is used, rather than passing a packed socket address in the
+C<addr> field, certain other hash keys may be used instead for convenience on
+certain named families.
+
+=over 4
+
+=cut
+
+use constant ADDRINFO_FAMILY => 0;
+use constant ADDRINFO_SOCKTYPE => 1;
+use constant ADDRINFO_PROTOCOL => 2;
+use constant ADDRINFO_ADDR => 3;
+
+sub extract_addrinfo
+{
+ my $self = shift;
+ my ( $ai, $argname ) = @_;
+
+ $argname ||= "addr";
+
+ my @ai;
+
+ if( ref $ai eq "ARRAY" ) {
+ @ai = @$ai;
+ }
+ elsif( ref $ai eq "HASH" ) {
+ $ai = { %$ai }; # copy so we can delete from it
+ @ai = delete @{$ai}{qw( family socktype protocol addr )};
+
+ if( defined $ai[ADDRINFO_FAMILY] and !defined $ai[ADDRINFO_ADDR] ) {
+ my $family = $ai[ADDRINFO_FAMILY];
+ my $method = "_extract_addrinfo_$family";
+ my $code = $self->can( $method ) or croak "Cannot determine addr for extract_addrinfo on family='$family'";
+
+ $ai[ADDRINFO_ADDR] = $code->( $self, $ai );
+
+ keys %$ai and croak "Unrecognised '$family' addrinfo keys: " . join( ", ", keys %$ai );
+ }
+ }
+ else {
+ croak "Expected '$argname' to be an ARRAY or HASH reference";
+ }
+
+ $ai[ADDRINFO_FAMILY] = $self->getfamilybyname( $ai[ADDRINFO_FAMILY] );
+ $ai[ADDRINFO_SOCKTYPE] = $self->getsocktypebyname( $ai[ADDRINFO_SOCKTYPE] );
+
+ # Make sure all fields are defined
+ $ai[$_] ||= 0 for ADDRINFO_FAMILY, ADDRINFO_SOCKTYPE, ADDRINFO_PROTOCOL;
+ $ai[ADDRINFO_ADDR] = "" if !defined $ai[ADDRINFO_ADDR];
+
+ return @ai;
+}
+
+=item family => 'inet'
+
+Will pack an IP address and port number from keys called C<ip> and C<port>.
+If C<ip> is missing it will be set to "0.0.0.0". If C<port> is missing it will
+be set to 0.
+
+=cut
+
+sub _extract_addrinfo_inet
+{
+ my $self = shift;
+ my ( $ai ) = @_;
+
+ my $port = delete $ai->{port} || 0;
+ my $ip = delete $ai->{ip} || "0.0.0.0";
+
+ return pack_sockaddr_in( $port, inet_aton( $ip ) );
+}
+
+=item family => 'inet6'
+
+Will pack an IP address and port number from keys called C<ip> and C<port>.
+If C<ip> is missing it will be set to "::". If C<port> is missing it will be
+set to 0. Optionally will also include values from C<scopeid> and C<flowinfo>
+keys if provided.
+
+This will only work if a C<pack_sockaddr_in6> function can be found in
+C<Socket>
+
+=cut
+
+sub _extract_addrinfo_inet6
+{
+ my $self = shift;
+ my ( $ai ) = @_;
+
+ my $port = delete $ai->{port} || 0;
+ my $ip = delete $ai->{ip} || "::";
+ my $scopeid = delete $ai->{scopeid} || 0;
+ my $flowinfo = delete $ai->{flowinfo} || 0;
+
+ if( HAVE_SOCKADDR_IN6 ) {
+ return pack_sockaddr_in6( $port, inet_pton( AF_INET6, $ip ), $scopeid, $flowinfo );
+ }
+ else {
+ croak "Cannot pack_sockaddr_in6";
+ }
+}
+
+=item family => 'unix'
+
+Will pack a UNIX socket path from a key called C<path>.
+
+=cut
+
+sub _extract_addrinfo_unix
+{
+ my $self = shift;
+ my ( $ai ) = @_;
+
+ defined( my $path = delete $ai->{path} ) or croak "Expected 'path' for extract_addrinfo on family='unix'";
+
+ return pack_sockaddr_un( $path );
+}
+
+=pod
+
+=back
+
+=cut
+
+=head1 LOOP IMPLEMENTATION METHODS
+
+The following methods are provided on C<IO::Async::OS> because they are likely
+to require OS-specific implementations, but are used by L<IO::Async::Loop> to
+implement its functionality. It can use the HASH reference C<< $loop->{os} >>
+to store other data it requires.
+
+=cut
+
+=head2 IO::Async::OS->loop_watch_signal( $loop, $signal, $code )
+
+=head2 IO::Async::OS->loop_unwatch_signal( $loop, $signal )
+
+Used to implement the C<watch_signal> / C<unwatch_signal> Loop pair.
+
+=cut
+
+sub loop_watch_signal
+{
+ my $self = shift;
+ my ( $loop, $signal, $code ) = @_;
+
+ exists $SIG{$signal} or croak "Unrecognised signal name $signal";
+ ref $code or croak 'Expected $code as a reference';
+
+ my $signum = $self->signame2num( $signal );
+ my $sigwatch = $loop->{os}{sigwatch} ||= {}; # {$num} = $code
+
+ my $sigpipe;
+ unless( $sigpipe = $loop->{os}{sigpipe} ) {
+ require IO::Async::Handle;
+
+ ( my $reader, $sigpipe ) = $self->pipepair or croak "Cannot pipe() - $!";
+ $_->blocking( 0 ) for $reader, $sigpipe;
+
+ $loop->{os}{sigpipe} = $sigpipe;
+
+ $loop->add( $loop->{os}{sigpipe_reader} = IO::Async::Handle->new(
+ notifier_name => "sigpipe",
+ read_handle => $reader,
+ on_read_ready => sub {
+ sysread $reader, my $buffer, 8192 or return;
+ foreach my $signum ( unpack "I*", $buffer ) {
+ $sigwatch->{$signum}->() if $sigwatch->{$signum};
+ }
+ },
+ ) );
+ }
+
+ my $signum_str = pack "I", $signum;
+ $SIG{$signal} = sub { syswrite $sigpipe, $signum_str };
+
+ $sigwatch->{$signum} = $code;
+}
+
+sub loop_unwatch_signal
+{
+ my $self = shift;
+ my ( $loop, $signal ) = @_;
+
+ my $signum = $self->signame2num( $signal );
+ my $sigwatch = $loop->{os}{sigwatch} or return;
+
+ delete $sigwatch->{$signum};
+ undef $SIG{$signal};
+}
+
+=head2 @fds = IO::Async::OS->potentially_open_fds
+
+Returns a list of filedescriptors which might need closing. By default this
+will return C<0 .. _SC_OPEN_MAX>. OS-specific subclasses may have a better
+guess.
+
+=cut
+
+sub potentially_open_fds
+{
+ return 0 .. OPEN_MAX_FD;
+}
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/OS/MSWin32.pm b/lib/IO/Async/OS/MSWin32.pm
new file mode 100644
index 0000000..bfed7f7
--- /dev/null
+++ b/lib/IO/Async/OS/MSWin32.pm
@@ -0,0 +1,111 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2012-2013 -- leonerd@leonerd.org.uk
+
+package IO::Async::OS::MSWin32;
+
+use strict;
+use warnings;
+
+our $VERSION = '0.67';
+
+our @ISA = qw( IO::Async::OS::_Base );
+
+use Carp;
+
+use Socket qw( AF_INET SOCK_STREAM SOCK_DGRAM INADDR_LOOPBACK pack_sockaddr_in );
+
+use IO::Socket (); # empty import
+
+use constant HAVE_FAKE_ISREG_READY => 1;
+
+# Also select() only reports connect() failures by evec, not wvec
+use constant HAVE_SELECT_CONNECT_EVEC => 1;
+
+use constant HAVE_POLL_CONNECT_POLLPRI => 1;
+
+use constant HAVE_CONNECT_EWOULDBLOCK => 1;
+
+use constant HAVE_RENAME_OPEN_FILES => 0;
+
+# poll(2) on Windows is emulated by wrapping select(2) anyway, so we might as
+# well try the Select loop first
+use constant LOOP_BUILTIN_CLASSES => qw( Select Poll );
+
+# CORE::fork() does not provide full POSIX semantics
+use constant HAVE_POSIX_FORK => 0;
+
+# Windows does not have signals, and SIGCHLD is not available
+use constant HAVE_SIGNALS => 0;
+
+=head1 NAME
+
+C<IO::Async::OS::MSWin32> - operating system abstractions on C<MSWin32> for C<IO::Async>
+
+=head1 DESCRIPTION
+
+This module contains OS support code for C<MSWin32>.
+
+See instead L<IO::Async::OS>.
+
+=cut
+
+# Win32's pipes don't actually work with select(). We'll have to create
+# sockets instead
+sub pipepair
+{
+ shift->socketpair( 'inet', 'stream' );
+}
+
+# Win32 doesn't have a socketpair(). We'll fake one up
+sub socketpair
+{
+ my $self = shift;
+ my ( $family, $socktype, $proto ) = @_;
+
+ $family = $self->getfamilybyname( $family ) || AF_INET;
+
+ # SOCK_STREAM is the most likely
+ $socktype = $self->getsocktypebyname( $socktype ) || SOCK_STREAM;
+
+ $proto ||= 0;
+
+ $family == AF_INET or croak "Cannot emulate ->socketpair except on AF_INET";
+
+ my $Stmp = $self->socket( $family, $socktype ) or return;
+ $Stmp->bind( pack_sockaddr_in( 0, INADDR_LOOPBACK ) ) or return;
+
+ my $S1 = $self->socket( $family, $socktype ) or return;
+
+ my $S2;
+ if( $socktype == SOCK_STREAM ) {
+ $Stmp->listen( 1 ) or return;
+ $S1->connect( getsockname $Stmp ) or return;
+ $S2 = $Stmp->accept or return;
+
+ # There's a bug in IO::Socket here, in that $S2 's ->socktype won't
+ # yet be set. We can apply a horribly hacky fix here
+ # defined $S2->socktype and $S2->socktype == $socktype or
+ # ${*$S2}{io_socket_type} = $socktype;
+ # But for now we'll skip the test for it instead
+ }
+ elsif( $socktype == SOCK_DGRAM ) {
+ $S2 = $Stmp;
+ $S1->connect( getsockname $S2 ) or return;
+ $S2->connect( getsockname $S1 ) or return;
+ }
+ else {
+ croak "Unrecognised socktype $socktype";
+ }
+
+ return ( $S1, $S2 );
+};
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/OS/cygwin.pm b/lib/IO/Async/OS/cygwin.pm
new file mode 100644
index 0000000..630bf9a
--- /dev/null
+++ b/lib/IO/Async/OS/cygwin.pm
@@ -0,0 +1,40 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2013 -- leonerd@leonerd.org.uk
+
+package IO::Async::OS::cygwin;
+
+use strict;
+use warnings;
+
+our $VERSION = '0.67';
+
+our @ISA = qw( IO::Async::OS::_Base );
+
+# Cygwin almost needs no hinting above the POSIX-like base, except that its
+# emulation of poll() isn't quite perfect. It needs POLLPRI
+use constant HAVE_POLL_CONNECT_POLLPRI => 1;
+
+# Also select() only reports connect() failures by evec, not wvec
+use constant HAVE_SELECT_CONNECT_EVEC => 1;
+
+=head1 NAME
+
+C<IO::Async::OS::cygwin> - operating system abstractions on C<cygwin> for C<IO::Async>
+
+=head1 DESCRIPTION
+
+This module contains OS support code for C<cygwin>.
+
+See instead L<IO::Async::OS>.
+
+=cut
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/OS/linux.pm b/lib/IO/Async/OS/linux.pm
new file mode 100644
index 0000000..c12949b
--- /dev/null
+++ b/lib/IO/Async/OS/linux.pm
@@ -0,0 +1,59 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2014-2015 -- leonerd@leonerd.org.uk
+
+package IO::Async::OS::linux;
+
+use strict;
+use warnings;
+
+our $VERSION = '0.67';
+
+our @ISA = qw( IO::Async::OS::_Base );
+
+=head1 NAME
+
+C<IO::Async::OS::linux> - operating system abstractions on C<Linux> for C<IO::Async>
+
+=head1 DESCRIPTION
+
+This module contains OS support code for C<Linux>.
+
+See instead L<IO::Async::OS>.
+
+=cut
+
+# Suggest either Epoll or Ppoll loops first if they are installed
+use constant LOOP_PREFER_CLASSES => qw( Epoll Ppoll );
+
+# Try to use /proc/pid/fd to get the list of actually-open file descriptors
+# for our process. Saves a bit of time when running with high ulimit -n /
+# fileno counts.
+sub potentially_open_fds
+{
+ my $class = shift;
+
+ opendir my $fd_path, "/proc/$$/fd" or do {
+ warn "Cannot open /proc/$$/fd, falling back to generic method - $!";
+ return $class->SUPER::potentially_open_fds
+ };
+
+ # Skip ., .., our directory handle itself and any other cruft
+ # except fileno() isn't available for the handle so we'll
+ # end up with that in the output anyway. As long as we're
+ # called just before the relevant close() loop, this
+ # should be harmless enough.
+ my @fd = map { m/^([0-9]+)$/ ? $1 : () } readdir $fd_path;
+ closedir $fd_path;
+
+ return @fd;
+}
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/PID.pm b/lib/IO/Async/PID.pm
new file mode 100644
index 0000000..fc59f9c
--- /dev/null
+++ b/lib/IO/Async/PID.pm
@@ -0,0 +1,196 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2010-2011 -- leonerd@leonerd.org.uk
+
+package IO::Async::PID;
+
+use strict;
+use warnings;
+use base qw( IO::Async::Notifier );
+
+our $VERSION = '0.67';
+
+use Carp;
+
+=head1 NAME
+
+C<IO::Async::PID> - event callback on exit of a child process
+
+=head1 SYNOPSIS
+
+ use IO::Async::PID;
+ use POSIX qw( WEXITSTATUS );
+
+ use IO::Async::Loop;
+ my $loop = IO::Async::Loop->new;
+
+ my $kid = $loop->fork(
+ code => sub {
+ print "Child sleeping..\n";
+ sleep 10;
+ print "Child exiting\n";
+ return 20;
+ },
+ );
+
+ print "Child process $kid started\n";
+
+ my $pid = IO::Async::PID->new(
+ pid => $kid,
+
+ on_exit => sub {
+ my ( $self, $exitcode ) = @_;
+ printf "Child process %d exited with status %d\n",
+ $self->pid, WEXITSTATUS($exitcode);
+ },
+ );
+
+ $loop->add( $pid );
+
+ $loop->run;
+
+=head1 DESCRIPTION
+
+This subclass of L<IO::Async::Notifier> invokes its callback when a process
+exits.
+
+For most use cases, a L<IO::Async::Process> object provides more control of
+setting up the process, connecting filehandles to it, sending data to and
+receiving data from it.
+
+=cut
+
+=head1 EVENTS
+
+The following events are invoked, either using subclass methods or CODE
+references in parameters:
+
+=head2 on_exit $exitcode
+
+Invoked when the watched process exits.
+
+=cut
+
+=head1 PARAMETERS
+
+The following named parameters may be passed to C<new> or C<configure>:
+
+=head2 pid => INT
+
+The process ID to watch. Must be given before the object has been added to the
+containing C<IO::Async::Loop> object.
+
+=head2 on_exit => CODE
+
+CODE reference for the C<on_exit> event.
+
+Once the C<on_exit> continuation has been invoked, the C<IO::Async::PID>
+object is removed from the containing C<IO::Async::Loop> object.
+
+=cut
+
+sub configure
+{
+ my $self = shift;
+ my %params = @_;
+
+ if( exists $params{pid} ) {
+ $self->loop and croak "Cannot configure 'pid' after adding to Loop";
+ $self->{pid} = delete $params{pid};
+ }
+
+ if( exists $params{on_exit} ) {
+ $self->{on_exit} = delete $params{on_exit};
+
+ undef $self->{cb};
+
+ if( my $loop = $self->loop ) {
+ $self->_remove_from_loop( $loop );
+ $self->_add_to_loop( $loop );
+ }
+ }
+
+ $self->SUPER::configure( %params );
+}
+
+sub _add_to_loop
+{
+ my $self = shift;
+ my ( $loop ) = @_;
+
+ $self->pid or croak "Require a 'pid' in $self";
+
+ $self->SUPER::_add_to_loop( @_ );
+
+ # on_exit continuation gets passed PID value; need to replace that with
+ # $self
+ $self->{cb} ||= $self->_replace_weakself( sub {
+ my $self = shift or return;
+ my ( $exitcode ) = @_;
+
+ $self->invoke_event( on_exit => $exitcode );
+
+ # Since this is a oneshot, we'll have to remove it from the loop or
+ # parent Notifier
+ $self->remove_from_parent;
+ } );
+
+ $loop->watch_child( $self->pid, $self->{cb} );
+}
+
+sub _remove_from_loop
+{
+ my $self = shift;
+ my ( $loop ) = @_;
+
+ $loop->unwatch_child( $self->pid );
+}
+
+sub notifier_name
+{
+ my $self = shift;
+ if( length( my $name = $self->SUPER::notifier_name ) ) {
+ return $name;
+ }
+
+ return $self->{pid};
+}
+
+=head1 METHODS
+
+=cut
+
+=head2 $process_id = $pid->pid
+
+Returns the underlying process ID
+
+=cut
+
+sub pid
+{
+ my $self = shift;
+ return $self->{pid};
+}
+
+=head2 $pid->kill( $signal )
+
+Sends a signal to the process
+
+=cut
+
+sub kill
+{
+ my $self = shift;
+ my ( $signal ) = @_;
+
+ kill $signal, $self->pid or croak "Cannot kill() - $!";
+}
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/Process.pm b/lib/IO/Async/Process.pm
new file mode 100644
index 0000000..31c70d5
--- /dev/null
+++ b/lib/IO/Async/Process.pm
@@ -0,0 +1,849 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2011-2015 -- leonerd@leonerd.org.uk
+
+package IO::Async::Process;
+
+use strict;
+use warnings;
+use base qw( IO::Async::Notifier );
+
+our $VERSION = '0.67';
+
+use Carp;
+
+use Socket qw( SOCK_STREAM );
+
+use Future;
+
+use IO::Async::OS;
+
+=head1 NAME
+
+C<IO::Async::Process> - start and manage a child process
+
+=head1 SYNOPSIS
+
+ use IO::Async::Process;
+
+ use IO::Async::Loop;
+ my $loop = IO::Async::Loop->new;
+
+ my $process = IO::Async::Process->new(
+ command => [ "tr", "a-z", "n-za-m" ],
+ stdin => {
+ from => "hello world\n",
+ },
+ stdout => {
+ on_read => sub {
+ my ( $stream, $buffref ) = @_;
+ while( $$buffref =~ s/^(.*)\n// ) {
+ print "Rot13 of 'hello world' is '$1'\n";
+ }
+
+ return 0;
+ },
+ },
+
+ on_finish => sub {
+ $loop->stop;
+ },
+ );
+
+ $loop->add( $process );
+
+ $loop->run;
+
+=head1 DESCRIPTION
+
+This subclass of L<IO::Async::Notifier> starts a child process, and invokes a
+callback when it exits. The child process can either execute a given block of
+code (via C<fork(2)>), or a command.
+
+=cut
+
+=head1 EVENTS
+
+The following events are invoked, either using subclass methods or CODE
+references in parameters:
+
+=head2 on_finish $exitcode
+
+Invoked after the process has exited by normal means (i.e. an C<exit(2)>
+syscall from a process, or C<return>ing from the code block), and has closed
+all its file descriptors.
+
+=head2 on_exception $exception, $errno, $exitcode
+
+Invoked when the process exits by an exception from C<code>, or by failing to
+C<exec(2)> the given command. C<$errno> will be a dualvar, containing both
+number and string values. After a successful C<exec()> call, this condition
+can no longer happen.
+
+Note that this has a different name and a different argument order from
+C<< Loop->open_child >>'s C<on_error>.
+
+If this is not provided and the process exits with an exception, then
+C<on_finish> is invoked instead, being passed just the exit code.
+
+Since this is just the results of the underlying C<< $loop->spawn_child >>
+C<on_exit> handler in a different order it is possible that the C<$exception>
+field will be an empty string. It will however always be defined. This can be
+used to distinguish the two cases:
+
+ on_exception => sub {
+ my ( $self, $exception, $errno, $exitcode ) = @_;
+
+ if( length $exception ) {
+ print STDERR "The process died with the exception $exception " .
+ "(errno was $errno)\n";
+ }
+ elsif( ( my $status = W_EXITSTATUS($exitcode) ) == 255 ) {
+ print STDERR "The process failed to exec() - $errno\n";
+ }
+ else {
+ print STDERR "The process exited with exit status $status\n";
+ }
+ }
+
+=cut
+
+=head1 CONSTRUCTOR
+
+=cut
+
+=head2 $process = IO::Async::Process->new( %args )
+
+Constructs a new C<IO::Async::Process> object and returns it.
+
+Once constructed, the C<Process> will need to be added to the C<Loop> before
+the child process is started.
+
+=cut
+
+sub _init
+{
+ my $self = shift;
+ $self->SUPER::_init( @_ );
+
+ $self->{to_close} = {};
+ $self->{finish_futures} = [];
+}
+
+=head1 PARAMETERS
+
+The following named parameters may be passed to C<new> or C<configure>:
+
+=head2 on_finish => CODE
+
+=head2 on_exception => CODE
+
+CODE reference for the event handlers.
+
+Once the C<on_finish> continuation has been invoked, the C<IO::Async::Process>
+object is removed from the containing C<IO::Async::Loop> object.
+
+The following parameters may be passed to C<new>, or to C<configure> before
+the process has been started (i.e. before it has been added to the C<Loop>).
+Once the process is running these cannot be changed.
+
+=head2 command => ARRAY or STRING
+
+Either a reference to an array containing the command and its arguments, or a
+plain string containing the command. This value is passed into perl's
+C<exec(2)> function.
+
+=head2 code => CODE
+
+A block of code to execute in the child process. It will be called in scalar
+context inside an C<eval> block.
+
+=head2 setup => ARRAY
+
+Optional reference to an array to pass to the underlying C<Loop>
+C<spawn_child> method.
+
+=head2 fdI<n> => HASH
+
+A hash describing how to set up file descriptor I<n>. The hash may contain the
+following keys:
+
+=over 4
+
+=item via => STRING
+
+Configures how this file descriptor will be configured for the child process.
+Must be given one of the following mode names:
+
+=over 4
+
+=item pipe_read
+
+The child will be given the writing end of a C<pipe(2)>; the parent may read
+from the other.
+
+=item pipe_write
+
+The child will be given the reading end of a C<pipe(2)>; the parent may write
+to the other. Since an EOF condition of this kind of handle cannot reliably be
+detected, C<on_finish> will not wait for this type of pipe to be closed.
+
+=item pipe_rdwr
+
+Only valid on the C<stdio> filehandle. The child will be given the reading end
+of one C<pipe(2)> on STDIN and the writing end of another on STDOUT. A single
+Stream object will be created in the parent configured for both filehandles.
+
+=item socketpair
+
+The child will be given one end of a C<socketpair(2)>; the parent will be
+given the other. The family of this socket may be given by the extra key
+called C<family>; defaulting to C<unix>. The socktype of this socket may be
+given by the extra key called C<socktype>; defaulting to C<stream>. If the
+type is not C<SOCK_STREAM> then a L<IO::Async::Socket> object will be
+constructed for the parent side of the handle, rather than
+C<IO::Async::Stream>.
+
+=back
+
+Once the filehandle is set up, the C<fd> method (or its shortcuts of C<stdin>,
+C<stdout> or C<stderr>) may be used to access the
+C<IO::Async::Handle>-subclassed object wrapped around it.
+
+The value of this argument is implied by any of the following alternatives.
+
+=item on_read => CODE
+
+The child will be given the writing end of a pipe. The reading end will be
+wrapped by an C<IO::Async::Stream> using this C<on_read> callback function.
+
+=item into => SCALAR
+
+The child will be given the writing end of a pipe. The referenced scalar will
+be filled by data read from the child process. This data may not be available
+until the pipe has been closed by the child.
+
+=item from => STRING
+
+The child will be given the reading end of a pipe. The string given by the
+C<from> parameter will be written to the child. When all of the data has been
+written the pipe will be closed.
+
+=back
+
+=head2 stdin => ...
+
+=head2 stdout => ...
+
+=head2 stderr => ...
+
+Shortcuts for C<fd0>, C<fd1> and C<fd2> respectively.
+
+=head2 stdio => ...
+
+Special filehandle to affect STDIN and STDOUT at the same time. This
+filehandle supports being configured for both reading and writing at the same
+time.
+
+=cut
+
+sub configure
+{
+ my $self = shift;
+ my %params = @_;
+
+ foreach (qw( on_finish on_exception )) {
+ $self->{$_} = delete $params{$_} if exists $params{$_};
+ }
+
+ # All these parameters can only be configured while the process isn't
+ # running
+ my %setup_params;
+ foreach (qw( code command setup stdin stdout stderr stdio ), grep { m/^fd\d+$/ } keys %params ) {
+ $setup_params{$_} = delete $params{$_} if exists $params{$_};
+ }
+
+ if( $self->is_running ) {
+ keys %setup_params and croak "Cannot configure a running Process with " . join ", ", keys %setup_params;
+ }
+
+ defined( exists $setup_params{code} ? $setup_params{code} : $self->{code} ) +
+ defined( exists $setup_params{command} ? $setup_params{command} : $self->{command} ) <= 1 or
+ croak "Cannot have both 'code' and 'command'";
+
+ foreach (qw( code command setup )) {
+ $self->{$_} = delete $setup_params{$_} if exists $setup_params{$_};
+ }
+
+ $self->configure_fd( 0, %{ delete $setup_params{stdin} } ) if $setup_params{stdin};
+ $self->configure_fd( 1, %{ delete $setup_params{stdout} } ) if $setup_params{stdout};
+ $self->configure_fd( 2, %{ delete $setup_params{stderr} } ) if $setup_params{stderr};
+
+ $self->configure_fd( 'io', %{ delete $setup_params{stdio} } ) if $setup_params{stdio};
+
+ # All the rest are fd\d+
+ foreach ( keys %setup_params ) {
+ my ( $fd ) = m/^fd(\d+)$/ or croak "Expected 'fd\\d+'";
+ $self->configure_fd( $fd, %{ $setup_params{$_} } );
+ }
+
+ $self->SUPER::configure( %params );
+}
+
+# These are from the perspective of the parent
+use constant FD_VIA_PIPEREAD => 1;
+use constant FD_VIA_PIPEWRITE => 2;
+use constant FD_VIA_PIPERDWR => 3; # Only valid for stdio pseudo-fd
+use constant FD_VIA_SOCKETPAIR => 4;
+
+my %via_names = (
+ pipe_read => FD_VIA_PIPEREAD,
+ pipe_write => FD_VIA_PIPEWRITE,
+ pipe_rdwr => FD_VIA_PIPERDWR,
+ socketpair => FD_VIA_SOCKETPAIR,
+);
+
+sub configure_fd
+{
+ my $self = shift;
+ my ( $fd, %args ) = @_;
+
+ $self->is_running and croak "Cannot configure fd $fd in a running Process";
+
+ if( $fd eq "io" ) {
+ exists $self->{fd_opts}{$_} and croak "Cannot configure stdio since fd$_ is already defined" for 0 .. 1;
+ }
+ elsif( $fd == 0 or $fd == 1 ) {
+ exists $self->{fd_opts}{io} and croak "Cannot configure fd$fd since stdio is already defined";
+ }
+
+ my $opts = $self->{fd_opts}{$fd} ||= {};
+ my $via = $opts->{via};
+
+ my ( $wants_read, $wants_write );
+
+ if( my $via_name = delete $args{via} ) {
+ defined $via and
+ croak "Cannot change the 'via' mode of fd$fd now that it is already configured";
+
+ $via = $via_names{$via_name} or
+ croak "Unrecognised 'via' name of '$via_name'";
+ }
+
+ if( my $on_read = delete $args{on_read} ) {
+ $opts->{handle}{on_read} = $on_read;
+
+ $wants_read++;
+ }
+ elsif( my $into = delete $args{into} ) {
+ $opts->{handle}{on_read} = sub {
+ my ( undef, $buffref, $eof ) = @_;
+ $$into .= $$buffref if $eof;
+ return 0;
+ };
+
+ $wants_read++;
+ }
+
+ if( defined( my $from = delete $args{from} ) ) {
+ $opts->{from} = $from;
+
+ $wants_write++;
+ }
+
+ if( defined $via and $via == FD_VIA_SOCKETPAIR ) {
+ $self->{fd_opts}{$fd}{$_} = delete $args{$_} for qw( family socktype );
+ }
+
+ keys %args and croak "Unexpected extra keys for fd $fd - " . join ", ", keys %args;
+
+ if( !defined $via ) {
+ $via = FD_VIA_PIPEREAD if $wants_read and !$wants_write;
+ $via = FD_VIA_PIPEWRITE if !$wants_read and $wants_write;
+ $via = FD_VIA_PIPERDWR if $wants_read and $wants_write;
+ }
+ elsif( $via == FD_VIA_PIPEREAD ) {
+ $wants_write and $via = FD_VIA_PIPERDWR;
+ }
+ elsif( $via == FD_VIA_PIPEWRITE ) {
+ $wants_read and $via = FD_VIA_PIPERDWR;
+ }
+ elsif( $via == FD_VIA_PIPERDWR or $via == FD_VIA_SOCKETPAIR ) {
+ # Fine
+ }
+ else {
+ die "Need to check fd_via{$fd}\n";
+ }
+
+ $via == FD_VIA_PIPERDWR and $fd ne "io" and
+ croak "Cannot both read and write simultaneously on fd$fd";
+
+ defined $via and $opts->{via} = $via;
+}
+
+sub _prepare_fds
+{
+ my $self = shift;
+ my ( $loop ) = @_;
+
+ my $fd_handle = $self->{fd_handle};
+ my $fd_opts = $self->{fd_opts};
+
+ my $finish_futures = $self->{finish_futures};
+
+ my @setup;
+
+ foreach my $fd ( keys %$fd_opts ) {
+ my $opts = $fd_opts->{$fd};
+ my $via = $opts->{via};
+
+ my $handle = $self->fd( $fd );
+
+ my $key = $fd eq "io" ? "stdio" : "fd$fd";
+ my $write_only;
+
+ if( $via == FD_VIA_PIPEREAD ) {
+ my ( $myfd, $childfd ) = IO::Async::OS->pipepair or croak "Unable to pipe() - $!";
+
+ $handle->configure( read_handle => $myfd );
+
+ push @setup, $key => [ dup => $childfd ];
+ $self->{to_close}{$childfd->fileno} = $childfd;
+ }
+ elsif( $via == FD_VIA_PIPEWRITE ) {
+ my ( $childfd, $myfd ) = IO::Async::OS->pipepair or croak "Unable to pipe() - $!";
+ $write_only++;
+
+ $handle->configure( write_handle => $myfd );
+
+ push @setup, $key => [ dup => $childfd ];
+ $self->{to_close}{$childfd->fileno} = $childfd;
+ }
+ elsif( $via == FD_VIA_PIPERDWR ) {
+ $key eq "stdio" or croak "Oops - should only be FD_VIA_PIPERDWR on stdio";
+ # Can't use pipequad here for now because we need separate FDs so we
+ # can ->close them properly
+ my ( $myread, $childwrite ) = IO::Async::OS->pipepair or croak "Unable to pipe() - $!";
+ my ( $childread, $mywrite ) = IO::Async::OS->pipepair or croak "Unable to pipe() - $!";
+
+ $handle->configure( read_handle => $myread, write_handle => $mywrite );
+
+ push @setup, stdin => [ dup => $childread ], stdout => [ dup => $childwrite ];
+ $self->{to_close}{$childread->fileno} = $childread;
+ $self->{to_close}{$childwrite->fileno} = $childwrite;
+ }
+ elsif( $via == FD_VIA_SOCKETPAIR ) {
+ my ( $myfd, $childfd ) = IO::Async::OS->socketpair( $opts->{family}, $opts->{socktype} ) or croak "Unable to socketpair() - $!";
+
+ $handle->configure( handle => $myfd );
+
+ if( $key eq "stdio" ) {
+ push @setup, stdin => [ dup => $childfd ], stdout => [ dup => $childfd ];
+ }
+ else {
+ push @setup, $key => [ dup => $childfd ];
+ }
+ $self->{to_close}{$childfd->fileno} = $childfd;
+ }
+ else {
+ croak "Unsure what to do with fd_via==$via";
+ }
+
+ $self->add_child( $handle );
+
+ unless( $write_only ) {
+ push @$finish_futures, $handle->new_close_future;
+ }
+ }
+
+ return @setup;
+}
+
+sub _add_to_loop
+{
+ my $self = shift;
+ my ( $loop ) = @_;
+
+ $self->{code} or $self->{command} or
+ croak "Require either 'code' or 'command' in $self";
+
+ $self->can_event( "on_finish" ) or
+ croak "Expected either an on_finish callback or to be able to ->on_finish";
+
+ my @setup;
+ push @setup, @{ $self->{setup} } if $self->{setup};
+
+ push @setup, $self->_prepare_fds( $loop );
+
+ my $finish_futures = delete $self->{finish_futures};
+
+ my ( $exitcode, $dollarbang, $dollarat );
+ push @$finish_futures, my $exit_future = $loop->new_future;
+
+ $self->{pid} = $loop->spawn_child(
+ code => $self->{code},
+ command => $self->{command},
+
+ setup => \@setup,
+
+ on_exit => $self->_capture_weakself( sub {
+ ( my $self, undef, $exitcode, $dollarbang, $dollarat ) = @_;
+
+ $self->debug_printf( "EXIT status=0x%04x", $exitcode ) if $self;
+ $exit_future->done unless $exit_future->is_cancelled;
+ } ),
+ );
+ $self->{running} = 1;
+
+ $self->SUPER::_add_to_loop( @_ );
+
+ $_->close for values %{ delete $self->{to_close} };
+
+ my $is_code = defined $self->{code};
+
+ $self->{finish_future} = Future->needs_all( @$finish_futures )
+ ->on_done( $self->_capture_weakself( sub {
+ my $self = shift or return;
+
+ $self->{exitcode} = $exitcode;
+ $self->{dollarbang} = $dollarbang;
+ $self->{dollarat} = $dollarat;
+
+ undef $self->{running};
+
+ if( $is_code ? $dollarat eq "" : $dollarbang == 0 ) {
+ $self->invoke_event( on_finish => $exitcode );
+ }
+ else {
+ $self->maybe_invoke_event( on_exception => $dollarat, $dollarbang, $exitcode ) or
+ # Don't have a way to report dollarbang/dollarat
+ $self->invoke_event( on_finish => $exitcode );
+ }
+
+ $self->remove_from_parent;
+ } ),
+ );
+}
+
+sub DESTROY
+{
+ my $self = shift;
+ $self->{finish_future}->cancel if $self->{finish_future};
+}
+
+sub notifier_name
+{
+ my $self = shift;
+ if( length( my $name = $self->SUPER::notifier_name ) ) {
+ return $name;
+ }
+
+ return "nopid" unless my $pid = $self->pid;
+ return "[$pid]" unless $self->is_running;
+ return "$pid";
+}
+
+=head1 METHODS
+
+=cut
+
+=head2 $pid = $process->pid
+
+Returns the process ID of the process, if it has been started, or C<undef> if
+not. Its value is preserved after the process exits, so it may be inspected
+during the C<on_finish> or C<on_exception> events.
+
+=cut
+
+sub pid
+{
+ my $self = shift;
+ return $self->{pid};
+}
+
+=head2 $process->kill( $signal )
+
+Sends a signal to the process
+
+=cut
+
+sub kill
+{
+ my $self = shift;
+ my ( $signal ) = @_;
+
+ kill $signal, $self->pid or croak "Cannot kill() - $!";
+}
+
+=head2 $running = $process->is_running
+
+Returns true if the Process has been started, and has not yet finished.
+
+=cut
+
+sub is_running
+{
+ my $self = shift;
+ return $self->{running};
+}
+
+=head2 $exited = $process->is_exited
+
+Returns true if the Process has finished running, and finished due to normal
+C<exit(2)>.
+
+=cut
+
+sub is_exited
+{
+ my $self = shift;
+ return defined $self->{exitcode} ? ( $self->{exitcode} & 0x7f ) == 0 : undef;
+}
+
+=head2 $status = $process->exitstatus
+
+If the process exited due to normal C<exit(2)>, returns the value that was
+passed to C<exit(2)>. Otherwise, returns C<undef>.
+
+=cut
+
+sub exitstatus
+{
+ my $self = shift;
+ return defined $self->{exitcode} ? ( $self->{exitcode} >> 8 ) : undef;
+}
+
+=head2 $exception = $process->exception
+
+If the process exited due to an exception, returns the exception that was
+thrown. Otherwise, returns C<undef>.
+
+=cut
+
+sub exception
+{
+ my $self = shift;
+ return $self->{dollarat};
+}
+
+=head2 $errno = $process->errno
+
+If the process exited due to an exception, returns the numerical value of
+C<$!> at the time the exception was thrown. Otherwise, returns C<undef>.
+
+=cut
+
+sub errno
+{
+ my $self = shift;
+ return $self->{dollarbang}+0;
+}
+
+=head2 $errstr = $process->errstr
+
+If the process exited due to an exception, returns the string value of
+C<$!> at the time the exception was thrown. Otherwise, returns C<undef>.
+
+=cut
+
+sub errstr
+{
+ my $self = shift;
+ return $self->{dollarbang}."";
+}
+
+=head2 $stream = $process->fd( $fd )
+
+Returns the L<IO::Async::Stream> or L<IO::Async::Socket> associated with the
+given FD number. This must have been set up by a C<configure> argument prior
+to adding the C<Process> object to the C<Loop>.
+
+The returned object have its read or write handle set to the other end of a
+pipe or socket connected to that FD number in the child process. Typically,
+this will be used to call the C<write> method on, to write more data into the
+child, or to set an C<on_read> handler to read data out of the child.
+
+The C<on_closed> event for these streams must not be changed, or it will break
+the close detection used by the C<Process> object and the C<on_finish> event
+will not be invoked.
+
+=cut
+
+sub fd
+{
+ my $self = shift;
+ my ( $fd ) = @_;
+
+ return $self->{fd_handle}{$fd} ||= do {
+ my $opts = $self->{fd_opts}{$fd} or
+ croak "$self does not have an fd Stream for $fd";
+
+ my $handle_class;
+ if( defined $opts->{socktype} && IO::Async::OS->getsocktypebyname( $opts->{socktype} ) != SOCK_STREAM ) {
+ require IO::Async::Socket;
+ $handle_class = "IO::Async::Socket";
+ }
+ else {
+ require IO::Async::Stream;
+ $handle_class = "IO::Async::Stream";
+ }
+
+ my $handle = $handle_class->new(
+ notifier_name => $fd eq "0" ? "stdin" :
+ $fd eq "1" ? "stdout" :
+ $fd eq "2" ? "stderr" :
+ $fd eq "io" ? "stdio" : "fd$fd",
+ %{ $opts->{handle} },
+ );
+
+ if( defined $opts->{from} ) {
+ $handle->write( $opts->{from},
+ on_flush => sub {
+ my ( $handle ) = @_;
+ $handle->close_write;
+ },
+ );
+ }
+
+ $handle
+ };
+}
+
+=head2 $stream = $process->stdin
+
+=head2 $stream = $process->stdout
+
+=head2 $stream = $process->stderr
+
+=head2 $stream = $process->stdio
+
+Shortcuts for calling C<fd> with 0, 1, 2 or C<io> respectively, to obtain the
+L<IO::Async::Stream> representing the standard input, output, error, or
+combined input/output streams of the child process.
+
+=cut
+
+sub stdin { shift->fd( 0 ) }
+sub stdout { shift->fd( 1 ) }
+sub stderr { shift->fd( 2 ) }
+sub stdio { shift->fd( 'io' ) }
+
+=head1 EXAMPLES
+
+=head2 Capturing the STDOUT stream of a process
+
+By configuring the C<stdout> filehandle of the process using the C<into> key,
+data written by the process can be captured.
+
+ my $stdout;
+ my $process = IO::Async::Process->new(
+ command => [ "writing-program", "arguments" ],
+ stdout => { into => \$stdout },
+ on_finish => sub {
+ print "The process has finished, and wrote:\n";
+ print $stdout;
+ }
+ );
+
+ $loop->add( $process );
+
+Note that until C<on_finish> is invoked, no guarantees are made about how much
+of the data actually written by the process is yet in the C<$stdout> scalar.
+
+See also the C<run_child> method of L<IO::Async::Loop>.
+
+To handle data more interactively as it arrives, the C<on_read> key can
+instead be used, to provide a callback function to invoke whenever more data
+is available from the process.
+
+ my $process = IO::Async::Process->new(
+ command => [ "writing-program", "arguments" ],
+ stdout => {
+ on_read => sub {
+ my ( $stream, $buffref ) = @_;
+ while( $$buffref =~ s/^(.*)\n// ) {
+ print "The process wrote a line: $1\n";
+ }
+
+ return 0;
+ },
+ },
+ on_finish => sub {
+ print "The process has finished\n";
+ }
+ );
+
+ $loop->add( $process );
+
+If the code to handle data read from the process isn't available yet when
+the object is constructed, it can be supplied later by using the C<configure>
+method on the C<stdout> filestream at some point before it gets added to the
+Loop. In this case, C<stdin> should be configured using C<pipe_read> in the
+C<via> key.
+
+ my $process = IO::Async::Process->new(
+ command => [ "writing-program", "arguments" ],
+ stdout => { via => "pipe_read" },
+ on_finish => sub {
+ print "The process has finished\n";
+ }
+ );
+
+ $process->stdout->configure(
+ on_read => sub {
+ my ( $stream, $buffref ) = @_;
+ while( $$buffref =~ s/^(.*)\n// ) {
+ print "The process wrote a line: $1\n";
+ }
+
+ return 0;
+ },
+ );
+
+ $loop->add( $process );
+
+=head2 Sending data to STDIN of a process
+
+By configuring the C<stdin> filehandle of the process using the C<from> key,
+data can be written into the C<STDIN> stream of the process.
+
+ my $process = IO::Async::Process->new(
+ command => [ "reading-program", "arguments" ],
+ stdin => { from => "Here is the data to send\n" },
+ on_finish => sub {
+ print "The process has finished\n";
+ }
+ );
+
+ $loop->add( $process );
+
+The data in this scalar will be written until it is all consumed, then the
+handle will be closed. This may be useful if the program waits for EOF on
+C<STDIN> before it exits.
+
+To have the ability to write more data into the process once it has started.
+the C<write> method on the C<stdin> stream can be used, when it is configured
+using the C<pipe_write> value for C<via>:
+
+ my $process = IO::Async::Process->new(
+ command => [ "reading-program", "arguments" ],
+ stdin => { via => "pipe_write" },
+ on_finish => sub {
+ print "The process has finished\n";
+ }
+ );
+
+ $loop->add( $process );
+
+ $process->stdin->write( "Here is some more data\n" );
+
+=cut
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/Protocol.pm b/lib/IO/Async/Protocol.pm
new file mode 100644
index 0000000..c155963
--- /dev/null
+++ b/lib/IO/Async/Protocol.pm
@@ -0,0 +1,259 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2011 -- leonerd@leonerd.org.uk
+
+package IO::Async::Protocol;
+
+use strict;
+use warnings;
+
+our $VERSION = '0.67';
+
+use base qw( IO::Async::Notifier );
+
+use Carp;
+
+=head1 NAME
+
+C<IO::Async::Protocol> - base class for transport-based protocols
+
+=head1 DESCRIPTION
+
+This subclass of L<IO::Async:Notifier> provides storage for a
+L<IO::Async::Handle> object, to act as a transport for some protocol. It
+contains an instance of the transport object, which it adds as a child
+notifier, allowing a level of independence from the actual transport being
+used. For example, a stream may actually be an L<IO::Async::SSLStream> to
+allow the protocol to be used over SSL.
+
+This class is not intended to be used directly, instead, see one of the
+subclasses
+
+=over 4
+
+=item L<IO::Async::Protocol::Stream> - base class for stream-based protocols
+
+=back
+
+=cut
+
+=head1 EVENTS
+
+The following events are invoked, either using subclass methods or CODE
+references in parameters:
+
+=head2 on_closed
+
+Optional. Invoked when the transport handle becomes closed.
+
+=cut
+
+=head1 PARAMETERS
+
+The following named parameters may be passed to C<new> or C<configure>:
+
+=head2 transport => IO::Async::Handle
+
+The C<IO::Async::Handle> to delegate communications to.
+
+=head2 on_closed => CODE
+
+CODE reference for the C<on_closed> event.
+
+When a new C<transport> object is given, it will be configured by calling the
+C<setup_transport> method, then added as a child notifier. If a different
+transport object was already configured, this will first be removed and
+deconfigured using the C<teardown_transport>.
+
+=cut
+
+sub configure
+{
+ my $self = shift;
+ my %params = @_;
+
+ for (qw( on_closed )) {
+ $self->{$_} = delete $params{$_} if exists $params{$_};
+ }
+
+ if( exists $params{transport} ) {
+ my $transport = delete $params{transport};
+
+ if( $self->{transport} ) {
+ $self->remove_child( $self->transport );
+
+ $self->teardown_transport( $self->transport );
+ }
+
+ $self->{transport} = $transport;
+
+ if( $transport ) {
+ $self->setup_transport( $self->transport );
+
+ $self->add_child( $self->transport );
+ }
+ }
+
+ $self->SUPER::configure( %params );
+}
+
+=head1 METHODS
+
+=cut
+
+=head2 $transport = $protocol->transport
+
+Returns the stored transport object
+
+=cut
+
+sub transport
+{
+ my $self = shift;
+ return $self->{transport};
+}
+
+=head2 $protocol->connect( %args )
+
+Sets up a connection to a peer, and configures the underlying C<transport> for
+the Protocol.
+
+Takes the following named arguments:
+
+=over 8
+
+=item socktype => STRING or INT
+
+Required. Identifies the socket type, and the type of continuation that will
+be used. If this value is C<"stream"> or C<SOCK_STREAM> then C<on_stream>
+continuation will be used; otherwise C<on_socket> will be used.
+
+=item on_connected => CODE
+
+Optional. If supplied, will be invoked once the connection has been
+established.
+
+ $on_connected->( $protocol )
+
+=item transport => IO::Async::Handle
+
+Optional. If this is provided, it will immediately be configured as the
+transport (by calling C<configure>), and the C<on_connected> callback will be
+invoked. This is provided as a convenient shortcut.
+
+=back
+
+Other arguments will be passed to the underlying C<IO::Async::Loop> C<connect>
+call.
+
+=cut
+
+sub connect
+{
+ my $self = shift;
+ my %args = @_;
+
+ my $on_connected = delete $args{on_connected};
+
+ if( my $transport = $args{transport} ) {
+ $self->configure( transport => $transport );
+
+ $on_connected->( $self ) if $on_connected;
+
+ return;
+ }
+
+ my $socktype = $args{socktype} or croak "Expected socktype";
+
+ my $on_transport = do {
+ no warnings 'numeric';
+ $socktype eq "stream" || $socktype == Socket::SOCK_STREAM()
+ } ? "on_stream" : "on_socket";
+
+ my $loop = $self->loop or croak "Cannot ->connect a ".ref($self)." that is not in a Loop";
+
+ $loop->connect(
+ %args,
+ socktype => "stream",
+
+ $on_transport => sub {
+ my ( $transport ) = @_;
+
+ $self->configure( transport => $transport );
+
+ $on_connected->( $self ) if $on_connected;
+ },
+ );
+}
+
+=head1 TRANSPORT DELEGATION
+
+The following methods are delegated to the transport object
+
+ close
+
+=cut
+
+sub close { shift->transport->close }
+
+=head1 SUBCLASS METHODS
+
+C<IO::Async::Protocol> is a base class provided so that specific subclasses of
+it provide more specific behaviour. The base class provides a number of
+methods that subclasses may wish to override.
+
+If a subclass implements any of these, be sure to invoke the superclass method
+at some point within the code.
+
+=cut
+
+=head2 $protocol->setup_transport( $transport )
+
+Called by C<configure> when a new C<transport> object is given, this method
+should perform whatever setup is required to wire the new transport object
+into the protocol object; typically by setting up event handlers.
+
+=cut
+
+sub setup_transport
+{
+ my $self = shift;
+ my ( $transport ) = @_;
+
+ $transport->configure(
+ on_closed => $self->_capture_weakself( sub {
+ my $self = shift or return;
+ my ( $transport ) = @_;
+
+ $self->maybe_invoke_event( on_closed => );
+
+ $self->configure( transport => undef );
+ } ),
+ );
+}
+
+=head2 $protocol->teardown_transport( $transport )
+
+The reverse of C<setup_transport>; called by C<configure> when a previously
+set-up transport object is about to be replaced.
+
+=cut
+
+sub teardown_transport
+{
+ my $self = shift;
+ my ( $transport ) = @_;
+
+ $transport->configure(
+ on_closed => undef,
+ );
+}
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/Protocol/LineStream.pm b/lib/IO/Async/Protocol/LineStream.pm
new file mode 100644
index 0000000..f6148e9
--- /dev/null
+++ b/lib/IO/Async/Protocol/LineStream.pm
@@ -0,0 +1,138 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2010 -- leonerd@leonerd.org.uk
+
+package IO::Async::Protocol::LineStream;
+
+use strict;
+use warnings;
+
+our $VERSION = '0.67';
+
+use base qw( IO::Async::Protocol::Stream );
+
+use Carp;
+
+=head1 NAME
+
+C<IO::Async::Protocol::LineStream> - stream-based protocols using lines of
+text
+
+=head1 SYNOPSIS
+
+Most likely this class will be subclassed to implement a particular network
+protocol.
+
+ package Net::Async::HelloWorld;
+
+ use strict;
+ use warnings;
+ use base qw( IO::Async::Protocol::LineStream );
+
+ sub on_read_line
+ {
+ my $self = shift;
+ my ( $line ) = @_;
+
+ if( $line =~ m/^HELLO (.*)/ ) {
+ my $name = $1;
+
+ $self->invoke_event( on_hello => $name );
+ }
+ }
+
+ sub send_hello
+ {
+ my $self = shift;
+ my ( $name ) = @_;
+
+ $self->write_line( "HELLO $name" );
+ }
+
+This small example elides such details as error handling, which a real
+protocol implementation would be likely to contain.
+
+=head1 DESCRIPTION
+
+=cut
+
+=head1 EVENTS
+
+The following events are invoked, either using subclass methods or CODE
+references in parameters:
+
+=head2 on_read_line $line
+
+Invoked when a new complete line of input is received.
+
+=cut
+
+=head1 PARAMETERS
+
+The following named parameters may be passed to C<new> or C<configure>:
+
+=head2 on_read_line => CODE
+
+CODE reference for the C<on_read_line> event.
+
+=cut
+
+sub _init
+{
+ my $self = shift;
+ $self->SUPER::_init;
+
+ $self->{eol} = "\x0d\x0a";
+ $self->{eol_pattern} = qr/\x0d?\x0a/;
+}
+
+sub configure
+{
+ my $self = shift;
+ my %params = @_;
+
+ foreach (qw( on_read_line )) {
+ $self->{$_} = delete $params{$_} if exists $params{$_};
+ }
+
+ $self->SUPER::configure( %params );
+}
+
+sub on_read
+{
+ my $self = shift;
+ my ( $buffref, $eof ) = @_;
+
+ # Easiest to run each event individually, in case it returns a CODE ref
+ $$buffref =~ s/^(.*?)$self->{eol_pattern}// or return 0;
+
+ return $self->invoke_event( on_read_line => $1 ) || 1;
+}
+
+=head1 METHODS
+
+=cut
+
+=head2 $lineprotocol->write_line( $text )
+
+Writes a line of text to the transport stream. The text will have the
+end-of-line marker appended to it; C<$text> should not end with it.
+
+=cut
+
+sub write_line
+{
+ my $self = shift;
+ my ( $line, @args ) = @_;
+
+ $self->write( "$line$self->{eol}", @args );
+}
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/Protocol/Stream.pm b/lib/IO/Async/Protocol/Stream.pm
new file mode 100644
index 0000000..11d1144
--- /dev/null
+++ b/lib/IO/Async/Protocol/Stream.pm
@@ -0,0 +1,237 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2010-2013 -- leonerd@leonerd.org.uk
+
+package IO::Async::Protocol::Stream;
+
+use strict;
+use warnings;
+
+our $VERSION = '0.67';
+
+use base qw( IO::Async::Protocol );
+
+use Carp;
+
+=head1 NAME
+
+C<IO::Async::Protocol::Stream> - base class for stream-based protocols
+
+=head1 SYNOPSIS
+
+Most likely this class will be subclassed to implement a particular network
+protocol.
+
+ package Net::Async::HelloWorld;
+
+ use strict;
+ use warnings;
+ use base qw( IO::Async::Protocol::Stream );
+
+ sub on_read
+ {
+ my $self = shift;
+ my ( $buffref, $eof ) = @_;
+
+ return 0 unless $$buffref =~ s/^(.*)\n//;
+ my $line = $1;
+
+ if( $line =~ m/^HELLO (.*)/ ) {
+ my $name = $1;
+
+ $self->invoke_event( on_hello => $name );
+ }
+
+ return 1;
+ }
+
+ sub send_hello
+ {
+ my $self = shift;
+ my ( $name ) = @_;
+
+ $self->write( "HELLO $name\n" );
+ }
+
+This small example elides such details as error handling, which a real
+protocol implementation would be likely to contain.
+
+=head1 DESCRIPTION
+
+This subclass of L<IO::Async::Protocol> is intended to stand as a base class
+for implementing stream-based protocols. It provides an interface similar to
+L<IO::Async::Stream>, primarily, a C<write> method and an C<on_read> event
+handler.
+
+It contains an instance of an C<IO::Async::Stream> object which it uses for
+actual communication, rather than being a subclass of it, allowing a level of
+independence from the actual stream being used. For example, the stream may
+actually be an L<IO::Async::SSLStream> to allow the protocol to be used over
+SSL.
+
+As with C<IO::Async::Stream>, it is required that by the time the protocol
+object is added to a Loop, that it either has an C<on_read> method, or has
+been configured with an C<on_read> callback handler.
+
+=cut
+
+=head1 EVENTS
+
+The following events are invoked, either using subclass methods or CODE
+references in parameters:
+
+=head2 $ret = on_read \$buffer, $eof
+
+=head2 on_read_eof
+
+=head2 on_write_eof
+
+The event handlers are invoked identically to C<IO::Async::Stream>.
+
+=head2 on_closed
+
+The C<on_closed> handler is optional, but if provided, will be invoked after
+the stream is closed by either side (either because the C<close()> method has
+been invoked on it, or on an incoming EOF).
+
+=cut
+
+=head1 PARAMETERS
+
+The following named parameters may be passed to C<new> or C<configure>:
+
+=head2 on_read => CODE
+
+=head2 on_read_eof => CODE
+
+=head2 on_write_eof => CODE
+
+CODE references for the events.
+
+=head2 handle => IO
+
+A shortcut for the common case where the transport only needs to be a plain
+C<IO::Async::Stream> object. If this argument is provided without a
+C<transport> object, a new C<IO::Async::Stream> object will be built around
+the given IO handle, and used as the transport.
+
+=cut
+
+sub configure
+{
+ my $self = shift;
+ my %params = @_;
+
+ for (qw( on_read on_read_eof on_write_eof )) {
+ $self->{$_} = delete $params{$_} if exists $params{$_};
+ }
+
+ if( !exists $params{transport} and my $handle = delete $params{handle} ) {
+ require IO::Async::Stream;
+ $params{transport} = IO::Async::Stream->new( handle => $handle );
+ }
+
+ $self->SUPER::configure( %params );
+
+ if( $self->loop ) {
+ $self->can_event( "on_read" ) or
+ croak 'Expected either an on_read callback or to be able to ->on_read';
+ }
+}
+
+sub _add_to_loop
+{
+ my $self = shift;
+
+ $self->can_event( "on_read" ) or
+ croak 'Expected either an on_read callback or to be able to ->on_read';
+}
+
+sub setup_transport
+{
+ my $self = shift;
+ my ( $transport ) = @_;
+
+ $self->SUPER::setup_transport( $transport );
+
+ $transport->configure(
+ on_read => $self->_replace_weakself( sub {
+ my $self = shift or return;
+ $self->invoke_event( on_read => @_ );
+ } ),
+ on_read_eof => $self->_replace_weakself( sub {
+ my $self = shift or return;
+ $self->maybe_invoke_event( on_read_eof => @_ );
+ } ),
+ on_write_eof => $self->_replace_weakself( sub {
+ my $self = shift or return;
+ $self->maybe_invoke_event( on_write_eof => @_ );
+ } ),
+ );
+}
+
+sub teardown_transport
+{
+ my $self = shift;
+ my ( $transport ) = @_;
+
+ $transport->configure(
+ on_read => undef,
+ );
+
+ $self->SUPER::teardown_transport( $transport );
+}
+
+=head1 METHODS
+
+=cut
+
+=head2 $protocol->write( $data )
+
+Writes the given data by calling the C<write> method on the contained
+transport stream.
+
+=cut
+
+sub write
+{
+ my $self = shift;
+ my ( $data, %args ) = @_;
+
+ if( ref $data eq "CODE" ) {
+ $data = $self->_replace_weakself( $data );
+ }
+
+ if( $args{on_flush} ) {
+ $args{on_flush} = $self->_replace_weakself( $args{on_flush} );
+ }
+
+ my $transport = $self->transport or croak "Attempted to ->write to a ".ref($self)." with no transport";
+ $transport->write( $data, %args );
+}
+
+=head2 $protocol->connect( %args )
+
+Sets up a connection to a peer, and configures the underlying C<transport> for
+the Protocol. Calls C<IO::Async::Protocol> C<connect> with C<socktype> set to
+C<"stream">.
+
+=cut
+
+sub connect
+{
+ my $self = shift;
+ $self->SUPER::connect(
+ @_,
+ socktype => "stream",
+ );
+}
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/Resolver.pm b/lib/IO/Async/Resolver.pm
new file mode 100644
index 0000000..10c0a15
--- /dev/null
+++ b/lib/IO/Async/Resolver.pm
@@ -0,0 +1,689 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2007-2015 -- leonerd@leonerd.org.uk
+
+package IO::Async::Resolver;
+
+use strict;
+use warnings;
+use 5.010;
+use base qw( IO::Async::Function );
+
+our $VERSION = '0.67';
+
+# Socket 2.006 fails to getaddrinfo() AI_NUMERICHOST properly on MSWin32
+use Socket 2.007 qw(
+ AI_NUMERICHOST AI_PASSIVE
+ NI_NUMERICHOST NI_NUMERICSERV NI_DGRAM
+ EAI_NONAME
+);
+
+use IO::Async::OS;
+
+# Try to use HiRes alarm, but we don't strictly need it.
+# MSWin32 doesn't implement it
+BEGIN {
+ require Time::HiRes;
+ eval { Time::HiRes::alarm(0) } and Time::HiRes->import( qw( alarm ) );
+}
+
+use Carp;
+
+my $started = 0;
+my %METHODS;
+
+=head1 NAME
+
+C<IO::Async::Resolver> - performing name resolutions asynchronously
+
+=head1 SYNOPSIS
+
+This object is used indirectly via an C<IO::Async::Loop>:
+
+ use IO::Async::Loop;
+ my $loop = IO::Async::Loop->new;
+
+ $loop->resolver->getaddrinfo(
+ host => "www.example.com",
+ service => "http",
+ )->on_done( sub {
+ foreach my $addr ( @_ ) {
+ printf "http://www.example.com can be reached at " .
+ "socket(%d,%d,%d) + connect('%v02x')\n",
+ @{$addr}{qw( family socktype protocol addr )};
+ }
+ });
+
+ $loop->resolve( type => 'getpwuid', data => [ $< ] )
+ ->on_done( sub {
+ print "My passwd ent: " . join( "|", @_ ) . "\n";
+ });
+
+ $loop->run;
+
+=head1 DESCRIPTION
+
+This module extends an C<IO::Async::Loop> to use the system's name resolver
+functions asynchronously. It provides a number of named resolvers, each one
+providing an asynchronous wrapper around a single resolver function.
+
+Because the system may not provide asynchronous versions of its resolver
+functions, this class is implemented using a C<IO::Async::Function> object
+that wraps the normal (blocking) functions. In this case, name resolutions
+will be performed asynchronously from the rest of the program, but will likely
+be done by a single background worker process, so will be processed in the
+order they were requested; a single slow lookup will hold up the queue of
+other requests behind it. To mitigate this, multiple worker processes can be
+used; see the C<workers> argument to the constructor.
+
+The C<idle_timeout> parameter for the underlying C<IO::Async::Function> object
+is set to a default of 30 seconds, and C<min_workers> is set to 0. This
+ensures that there are no spare processes sitting idle during the common case
+of no outstanding requests.
+
+=cut
+
+sub _init
+{
+ my $self = shift;
+ my ( $params ) = @_;
+ $self->SUPER::_init( @_ );
+
+ $params->{code} = sub {
+ my ( $type, $timeout, @data ) = @_;
+
+ if( my $code = $METHODS{$type} ) {
+ local $SIG{ALRM} = sub { die "Timed out\n" };
+
+ alarm( $timeout );
+ my @ret = eval { $code->( @data ) };
+ alarm( 0 );
+
+ die $@ if $@;
+ return @ret;
+ }
+ else {
+ die "Unrecognised resolver request '$type'";
+ }
+ };
+
+ $params->{idle_timeout} = 30;
+ $params->{min_workers} = 0;
+
+ $started = 1;
+}
+
+=head1 METHODS
+
+The following methods documented with a trailing call to C<< ->get >> return
+L<Future> instances.
+
+=cut
+
+=head2 @result = $loop->resolve( %params )->get
+
+Performs a single name resolution operation, as given by the keys in the hash.
+
+The C<%params> hash keys the following keys:
+
+=over 8
+
+=item type => STRING
+
+Name of the resolution operation to perform. See BUILT-IN RESOLVERS for the
+list of available operations.
+
+=item data => ARRAY
+
+Arguments to pass to the resolver function. Exact meaning depends on the
+specific function chosen by the C<type>; see BUILT-IN RESOLVERS.
+
+=item timeout => NUMBER
+
+Optional. Timeout in seconds, after which the resolver operation will abort
+with a timeout exception. If not supplied, a default of 10 seconds will apply.
+
+=back
+
+=head2 $resolver->resolve( %params )
+
+When not returning a future, additional parameters can be given containing the
+continuations to invoke on success or failure:
+
+=over 8
+
+=item on_resolved => CODE
+
+A continuation that is invoked when the resolver function returns a successful
+result. It will be passed the array returned by the resolver function.
+
+ $on_resolved->( @result )
+
+=item on_error => CODE
+
+A continuation that is invoked when the resolver function fails. It will be
+passed the exception thrown by the function.
+
+=back
+
+=cut
+
+sub resolve
+{
+ my $self = shift;
+ my %args = @_;
+
+ my $type = $args{type};
+ defined $type or croak "Expected 'type'";
+
+ if( $type eq "getaddrinfo" ) {
+ $type = "getaddrinfo_hash";
+ }
+
+ exists $METHODS{$type} or croak "Expected 'type' to be an existing resolver method, got '$type'";
+
+ my $on_resolved;
+ if( $on_resolved = $args{on_resolved} ) {
+ ref $on_resolved or croak "Expected 'on_resolved' to be a reference";
+ }
+ elsif( !defined wantarray ) {
+ croak "Expected 'on_resolved' or to return a Future";
+ }
+
+ my $on_error;
+ if( $on_error = $args{on_error} ) {
+ ref $on_error or croak "Expected 'on_error' to be a reference";
+ }
+ elsif( !defined wantarray ) {
+ croak "Expected 'on_error' or to return a Future";
+ }
+
+ my $timeout = $args{timeout} || 10;
+
+ my $future = $self->call(
+ args => [ $type, $timeout, @{$args{data}} ],
+ );
+
+ $future->on_done( $on_resolved ) if $on_resolved;
+ $future->on_fail( $on_error ) if $on_error;
+
+ return $future if defined wantarray;
+
+ # Caller is not going to keep hold of the Future, so we have to ensure it
+ # stays alive somehow
+ $self->adopt_future( $future->else( sub { Future->done } ) );
+}
+
+=head2 @addrs = $resolver->getaddrinfo( %args )->get
+
+A shortcut wrapper around the C<getaddrinfo> resolver, taking its arguments in
+a more convenient form.
+
+=over 8
+
+=item host => STRING
+
+=item service => STRING
+
+The host and service names to look up. At least one must be provided.
+
+=item family => INT or STRING
+
+=item socktype => INT or STRING
+
+=item protocol => INT
+
+Hint values used to filter the results.
+
+=item flags => INT
+
+Flags to control the C<getaddrinfo(3)> function. See the C<AI_*> constants in
+L<Socket>'s C<getaddrinfo> function for more detail.
+
+=item passive => BOOL
+
+If true, sets the C<AI_PASSIVE> flag. This is provided as a convenience to
+avoid the caller from having to import the C<AI_PASSIVE> constant from
+C<Socket>.
+
+=item timeout => NUMBER
+
+Time in seconds after which to abort the lookup with a C<Timed out> exception
+
+=back
+
+On success, the future will yield the result as a list of HASH references;
+each containing one result. Each result will contain fields called C<family>,
+C<socktype>, C<protocol> and C<addr>. If requested by C<AI_CANONNAME> then the
+C<canonname> field will also be present.
+
+As a specific optimisation, this method will try to perform a lookup of
+numeric values synchronously, rather than asynchronously, if it looks likely
+to succeed.
+
+Specifically, if the service name is entirely numeric, and the hostname looks
+like an IPv4 or IPv6 string, a synchronous lookup will first be performed
+using the C<AI_NUMERICHOST> flag. If this gives an C<EAI_NONAME> error, then
+the lookup is performed asynchronously instead.
+
+=head2 $resolver->getaddrinfo( %args )
+
+When not returning a future, additional parameters can be given containing the
+continuations to invoke on success or failure:
+
+=over 8
+
+=item on_resolved => CODE
+
+Callback which is invoked after a successful lookup.
+
+ $on_resolved->( @addrs )
+
+=item on_error => CODE
+
+Callback which is invoked after a failed lookup, including for a timeout.
+
+ $on_error->( $exception )
+
+=back
+
+=cut
+
+sub getaddrinfo
+{
+ my $self = shift;
+ my %args = @_;
+
+ $args{on_resolved} or defined wantarray or
+ croak "Expected 'on_resolved' or to return a Future";
+
+ $args{on_error} or defined wantarray or
+ croak "Expected 'on_error' or to return a Future";
+
+ my $host = $args{host} || "";
+ my $service = $args{service} // "";
+ my $flags = $args{flags} || 0;
+
+ $flags |= AI_PASSIVE if $args{passive};
+
+ $args{family} = IO::Async::OS->getfamilybyname( $args{family} ) if defined $args{family};
+ $args{socktype} = IO::Async::OS->getsocktypebyname( $args{socktype} ) if defined $args{socktype};
+
+ # Clear any other existing but undefined hints
+ defined $args{$_} or delete $args{$_} for keys %args;
+
+ # It's likely this will succeed with AI_NUMERICHOST if host contains only
+ # [\d.] (IPv4) or [[:xdigit:]:] (IPv6)
+ # Technically we should pass AI_NUMERICSERV but not all platforms support
+ # it, but since we're checking service contains only \d we should be fine.
+
+ # These address tests don't have to be perfect as if it fails we'll get
+ # EAI_NONAME and just try it asynchronously anyway
+ if( ( $host =~ m/^[\d.]+$/ or $host =~ m/^[[:xdigit:]:]$/ or $host eq "" ) and
+ $service =~ m/^\d+$/ ) {
+
+ my ( $err, @results ) = Socket::getaddrinfo( $host, $service,
+ { %args, flags => $flags | AI_NUMERICHOST }
+ );
+
+ if( !$err ) {
+ my $future = $self->loop->new_future->done( @results );
+ $future->on_done( $args{on_resolved} ) if $args{on_resolved};
+ return $future;
+ }
+ elsif( $err == EAI_NONAME ) {
+ # fallthrough to async case
+ }
+ else {
+ my $future = $self->loop->new_future->fail( $err, resolve => getaddrinfo => $err+0 );
+ $future->on_fail( $args{on_error} ) if $args{on_error};
+ return $future;
+ }
+ }
+
+ my $future = $self->resolve(
+ type => "getaddrinfo_hash",
+ data => [
+ host => $host,
+ service => $service,
+ flags => $flags,
+ map { exists $args{$_} ? ( $_ => $args{$_} ) : () } qw( family socktype protocol ),
+ ],
+ timeout => $args{timeout},
+ )->else( sub {
+ my $message = shift;
+ Future->fail( $message, resolve => getaddrinfo => @_ );
+ });
+
+ $future->on_done( $args{on_resolved} ) if $args{on_resolved};
+ $future->on_fail( $args{on_error} ) if $args{on_error};
+
+ return $future if defined wantarray;
+
+ # Caller is not going to keep hold of the Future, so we have to ensure it
+ # stays alive somehow
+ $self->adopt_future( $future->else( sub { Future->done } ) );
+}
+
+=head2 ( $host, $service ) = $resolver->getnameinfo( %args )->get
+
+A shortcut wrapper around the C<getnameinfo> resolver, taking its arguments in
+a more convenient form.
+
+=over 8
+
+=item addr => STRING
+
+The packed socket address to look up.
+
+=item flags => INT
+
+Flags to control the C<getnameinfo(3)> function. See the C<NI_*> constants in
+L<Socket>'s C<getnameinfo> for more detail.
+
+=item numerichost => BOOL
+
+=item numericserv => BOOL
+
+=item dgram => BOOL
+
+If true, set the C<NI_NUMERICHOST>, C<NI_NUMERICSERV> or C<NI_DGRAM> flags.
+
+=item numeric => BOOL
+
+If true, sets both C<NI_NUMERICHOST> and C<NI_NUMERICSERV> flags.
+
+=item timeout => NUMBER
+
+Time in seconds after which to abort the lookup with a C<Timed out> exception
+
+=back
+
+As a specific optimisation, this method will try to perform a lookup of
+numeric values synchronously, rather than asynchronously, if both the
+C<NI_NUMERICHOST> and C<NI_NUMERICSERV> flags are given.
+
+=head2 $resolver->getnameinfo( %args )
+
+When not returning a future, additional parameters can be given containing the
+continuations to invoke on success or failure:
+
+=over 8
+
+=item on_resolved => CODE
+
+Callback which is invoked after a successful lookup.
+
+ $on_resolved->( $host, $service )
+
+=item on_error => CODE
+
+Callback which is invoked after a failed lookup, including for a timeout.
+
+ $on_error->( $exception )
+
+=back
+
+=cut
+
+sub getnameinfo
+{
+ my $self = shift;
+ my %args = @_;
+
+ $args{on_resolved} or defined wantarray or
+ croak "Expected 'on_resolved' or to return a Future";
+
+ $args{on_error} or defined wantarray or
+ croak "Expected 'on_error' or to return a Future";
+
+ my $flags = $args{flags} || 0;
+
+ $flags |= NI_NUMERICHOST if $args{numerichost};
+ $flags |= NI_NUMERICSERV if $args{numericserv};
+ $flags |= NI_DGRAM if $args{dgram};
+
+ $flags |= NI_NUMERICHOST|NI_NUMERICSERV if $args{numeric};
+
+ if( $flags & (NI_NUMERICHOST|NI_NUMERICSERV) ) {
+ # This is a numeric-only lookup that can be done synchronously
+ my ( $err, $host, $service ) = Socket::getnameinfo( $args{addr}, $flags );
+
+ if( $err ) {
+ my $future = $self->loop->new_future->fail( $err, resolve => getnameinfo => $err+0 );
+ $future->on_fail( $args{on_error} ) if $args{on_error};
+ return $future;
+ }
+ else {
+ my $future = $self->loop->new_future->done( $host, $service );
+ $future->on_done( $args{on_resolved} ) if $args{on_resolved};
+ return $future;
+ }
+ }
+
+ my $future = $self->resolve(
+ type => "getnameinfo",
+ data => [ $args{addr}, $flags ],
+ timeout => $args{timeout},
+ )->transform(
+ done => sub { @{ $_[0] } }, # unpack the ARRAY ref
+ )->else( sub {
+ my $message = shift;
+ Future->fail( $message, resolve => getnameinfo => @_ );
+ });
+
+ $future->on_done( $args{on_resolved} ) if $args{on_resolved};
+ $future->on_fail( $args{on_error} ) if $args{on_error};
+
+ return $future if defined wantarray;
+
+ # Caller is not going to keep hold of the Future, so we have to ensure it
+ # stays alive somehow
+ $self->adopt_future( $future->else( sub { Future->done } ) );
+}
+
+=head1 FUNCTIONS
+
+=cut
+
+=head2 register_resolver( $name, $code )
+
+Registers a new named resolver function that can be called by the C<resolve>
+method. All named resolvers must be registered before the object is
+constructed.
+
+=over 8
+
+=item $name
+
+The name of the resolver function; must be a plain string. This name will be
+used by the C<type> argument to the C<resolve> method, to identify it.
+
+=item $code
+
+A CODE reference to the resolver function body. It will be called in list
+context, being passed the list of arguments given in the C<data> argument to
+the C<resolve> method. The returned list will be passed to the
+C<on_resolved> callback. If the code throws an exception at call time, it will
+be passed to the C<on_error> continuation. If it returns normally, the list of
+values it returns will be passed to C<on_resolved>.
+
+=back
+
+=cut
+
+# Plain function, not a method
+sub register_resolver
+{
+ my ( $name, $code ) = @_;
+
+ croak "Cannot register new resolver methods once the resolver has been started" if $started;
+
+ croak "Already have a resolver method called '$name'" if exists $METHODS{$name};
+ $METHODS{$name} = $code;
+}
+
+=head1 BUILT-IN RESOLVERS
+
+The following resolver names are implemented by the same-named perl function,
+taking and returning a list of values exactly as the perl function does:
+
+ getpwnam getpwuid
+ getgrnam getgrgid
+ getservbyname getservbyport
+ gethostbyname gethostbyaddr
+ getnetbyname getnetbyaddr
+ getprotobyname getprotobynumber
+
+=cut
+
+# Now register the inbuilt methods
+
+register_resolver getpwnam => sub { my @r = getpwnam( $_[0] ) or die "$!\n"; @r };
+register_resolver getpwuid => sub { my @r = getpwuid( $_[0] ) or die "$!\n"; @r };
+
+register_resolver getgrnam => sub { my @r = getgrnam( $_[0] ) or die "$!\n"; @r };
+register_resolver getgrgid => sub { my @r = getgrgid( $_[0] ) or die "$!\n"; @r };
+
+register_resolver getservbyname => sub { my @r = getservbyname( $_[0], $_[1] ) or die "$!\n"; @r };
+register_resolver getservbyport => sub { my @r = getservbyport( $_[0], $_[1] ) or die "$!\n"; @r };
+
+register_resolver gethostbyname => sub { my @r = gethostbyname( $_[0] ) or die "$!\n"; @r };
+register_resolver gethostbyaddr => sub { my @r = gethostbyaddr( $_[0], $_[1] ) or die "$!\n"; @r };
+
+register_resolver getnetbyname => sub { my @r = getnetbyname( $_[0] ) or die "$!\n"; @r };
+register_resolver getnetbyaddr => sub { my @r = getnetbyaddr( $_[0], $_[1] ) or die "$!\n"; @r };
+
+register_resolver getprotobyname => sub { my @r = getprotobyname( $_[0] ) or die "$!\n"; @r };
+register_resolver getprotobynumber => sub { my @r = getprotobynumber( $_[0] ) or die "$!\n"; @r };
+
+=pod
+
+The following three resolver names are implemented using the L<Socket> module.
+
+ getaddrinfo_hash
+ getaddrinfo_array
+ getnameinfo
+
+The C<getaddrinfo_hash> resolver takes arguments in a hash of name/value pairs
+and returns a list of hash structures, as the C<Socket::getaddrinfo> function
+does. For neatness it takes all its arguments as named values; taking the host
+and service names from arguments called C<host> and C<service> respectively;
+all the remaining arguments are passed into the hints hash. This name is also
+aliased as simply C<getaddrinfo>.
+
+The C<getaddrinfo_array> resolver behaves more like the C<Socket6> version of
+the function. It takes hints in a flat list, and mangles the result of the
+function, so that the returned value is more useful to the caller. It splits
+up the list of 5-tuples into a list of ARRAY refs, where each referenced array
+contains one of the tuples of 5 values.
+
+As an extra convenience to the caller, both resolvers will also accept plain
+string names for the C<family> argument, converting C<inet> and possibly
+C<inet6> into the appropriate C<AF_*> value, and for the C<socktype> argument,
+converting C<stream>, C<dgram> or C<raw> into the appropriate C<SOCK_*> value.
+
+The C<getnameinfo> resolver returns its result in the same form as C<Socket>.
+
+Because this module simply uses the system's C<getaddrinfo> resolver, it will
+be fully IPv6-aware if the underlying platform's resolver is. This allows
+programs to be fully IPv6-capable.
+
+=cut
+
+register_resolver getaddrinfo_hash => sub {
+ my %args = @_;
+
+ my $host = delete $args{host};
+ my $service = delete $args{service};
+
+ $args{family} = IO::Async::OS->getfamilybyname( $args{family} ) if defined $args{family};
+ $args{socktype} = IO::Async::OS->getsocktypebyname( $args{socktype} ) if defined $args{socktype};
+
+ # Clear any other existing but undefined hints
+ defined $args{$_} or delete $args{$_} for keys %args;
+
+ my ( $err, @addrs ) = Socket::getaddrinfo( $host, $service, \%args );
+
+ die "$err\n" if $err;
+
+ return @addrs;
+};
+
+register_resolver getaddrinfo_array => sub {
+ my ( $host, $service, $family, $socktype, $protocol, $flags ) = @_;
+
+ $family = IO::Async::OS->getfamilybyname( $family );
+ $socktype = IO::Async::OS->getsocktypebyname( $socktype );
+
+ my %hints;
+ $hints{family} = $family if defined $family;
+ $hints{socktype} = $socktype if defined $socktype;
+ $hints{protocol} = $protocol if defined $protocol;
+ $hints{flags} = $flags if defined $flags;
+
+ my ( $err, @addrs ) = Socket::getaddrinfo( $host, $service, \%hints );
+
+ die "$err\n" if $err;
+
+ # Convert the @addrs list into a list of ARRAY refs of 5 values each
+ return map {
+ [ $_->{family}, $_->{socktype}, $_->{protocol}, $_->{addr}, $_->{canonname} ]
+ } @addrs;
+};
+
+register_resolver getnameinfo => sub {
+ my ( $addr, $flags ) = @_;
+
+ my ( $err, $host, $service ) = Socket::getnameinfo( $addr, $flags || 0 );
+
+ die "$err\n" if $err;
+
+ return [ $host, $service ];
+};
+
+=head1 EXAMPLES
+
+The following somewhat contrieved example shows how to implement a new
+resolver function. This example just uses in-memory data, but a real function
+would likely make calls to OS functions to provide an answer. In traditional
+Unix style, a pair of functions are provided that each look up the entity by
+either type of key, where both functions return the same type of list. This is
+purely a convention, and is in no way required or enforced by the
+C<IO::Async::Resolver> itself.
+
+ @numbers = qw( zero one two three four
+ five six seven eight nine );
+
+ register_resolver getnumberbyindex => sub {
+ my ( $index ) = @_;
+ die "Bad index $index" unless $index >= 0 and $index < @numbers;
+ return ( $index, $numbers[$index] );
+ };
+
+ register_resolver getnumberbyname => sub {
+ my ( $name ) = @_;
+ foreach my $index ( 0 .. $#numbers ) {
+ return ( $index, $name ) if $numbers[$index] eq $name;
+ }
+ die "Bad name $name";
+ };
+
+=head1 TODO
+
+=over 4
+
+=item *
+
+Look into (system-specific) ways of accessing asynchronous resolvers directly
+
+=back
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/Routine.pm b/lib/IO/Async/Routine.pm
new file mode 100644
index 0000000..f9a5a3b
--- /dev/null
+++ b/lib/IO/Async/Routine.pm
@@ -0,0 +1,436 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2012-2013 -- leonerd@leonerd.org.uk
+
+package IO::Async::Routine;
+
+use strict;
+use warnings;
+
+our $VERSION = '0.67';
+
+use base qw( IO::Async::Notifier );
+
+use Carp;
+
+use IO::Async::OS;
+use IO::Async::Process;
+
+=head1 NAME
+
+C<IO::Async::Routine> - execute code in an independent sub-process or thread
+
+=head1 SYNOPSIS
+
+ use IO::Async::Routine;
+ use IO::Async::Channel;
+
+ use IO::Async::Loop;
+ my $loop = IO::Async::Loop->new;
+
+ my $nums_ch = IO::Async::Channel->new;
+ my $ret_ch = IO::Async::Channel->new;
+
+ my $routine = IO::Async::Routine->new(
+ channels_in => [ $nums_ch ],
+ channels_out => [ $ret_ch ],
+
+ code => sub {
+ my @nums = @{ $nums_ch->recv };
+ my $ret = 0; $ret += $_ for @nums;
+
+ # Can only send references
+ $ret_ch->send( \$ret );
+ },
+
+ on_finish => sub {
+ say "The routine aborted early - $_[-1]";
+ $loop->stop;
+ },
+ );
+
+ $loop->add( $routine );
+
+ $nums_ch->send( [ 10, 20, 30 ] );
+ $ret_ch->recv(
+ on_recv => sub {
+ my ( $ch, $totalref ) = @_;
+ say "The total of 10, 20, 30 is: $$totalref";
+ $loop->stop;
+ }
+ );
+
+ $loop->run;
+
+=head1 DESCRIPTION
+
+This L<IO::Async::Notifier> contains a body of code and executes it in a
+sub-process or thread, allowing it to act independently of the main program.
+Once set up, all communication with the code happens by values passed into or
+out of the Routine via L<IO::Async::Channel> objects.
+
+A choice of detachment model is available, with options being a C<fork()>ed
+child process, or a thread. In both cases the code contained within the
+Routine is free to make blocking calls without stalling the rest of the
+program. This makes it useful for using existing code which has no option not
+to block within an C<IO::Async>-based program.
+
+Code running inside a C<fork()>-based Routine runs within its own process; it
+is isolated from the rest of the program in terms of memory, CPU time, and
+other resources. Code running in a thread-based Routine however, shares memory
+and other resources such as open filehandles with the main thread.
+
+To create asynchronous wrappers of functions that return a value based only on
+their arguments, and do not generally maintain state within the process it may
+be more convenient to use an L<IO::Async::Function> instead, which uses an
+C<IO::Async::Routine> to contain the body of the function and manages the
+Channels itself.
+
+=cut
+
+=head1 EVENTS
+
+=head2 on_finish $exitcode
+
+For C<fork()>-based Routines, this is invoked after the process has exited and
+is passed the raw exitcode status.
+
+=head2 on_finish $type, @result
+
+For thread-based Routines, this is invoked after the thread has returned from
+its code block and is passed the C<on_joined> result.
+
+As the behaviour of these events differs per model, it may be more convenient
+to use C<on_return> and C<on_die> instead.
+
+=head2 on_return $result
+
+Invoked if the code block returns normally. Note that C<fork()>-based Routines
+can only transport an integer result between 0 and 255, as this is the actual
+C<exit()> value.
+
+=head2 on_die $exception
+
+Invoked if the code block fails with an exception.
+
+=cut
+
+=head1 PARAMETERS
+
+The following named parameters may be passed to C<new> or C<configure>:
+
+=head2 model => "fork" | "thread"
+
+Optional. Defines how the routine will detach itself from the main process.
+C<fork> uses a child process detached using an L<IO::Async::Process>.
+C<thread> uses a thread, and is only available on threaded Perls.
+
+If the model is not specified, the environment variable
+C<IO_ASYNC_ROUTINE_MODEL> is used to pick a default. If that isn't defined,
+C<fork> is preferred if it is available, otherwise C<thread>.
+
+=head2 channels_in => ARRAY of IO::Async::Channel
+
+ARRAY reference of C<IO::Async::Channel> objects to set up for passing values
+in to the Routine.
+
+=head2 channels_out => ARRAY of IO::Async::Channel
+
+ARRAY reference of C<IO::Async::Channel> objects to set up for passing values
+out of the Routine.
+
+=head2 code => CODE
+
+CODE reference to the body of the Routine, to execute once the channels are
+set up.
+
+=head2 setup => ARRAY
+
+Optional. For C<fork()>-based Routines, gives a reference to an array to pass
+to the underlying C<Loop> C<fork_child> method. Ignored for thread-based
+Routines.
+
+=cut
+
+use constant PREFERRED_MODEL =>
+ IO::Async::OS->HAVE_POSIX_FORK ? "fork" :
+ IO::Async::OS->HAVE_THREADS ? "thread" :
+ die "No viable Routine models";
+
+sub _init
+{
+ my $self = shift;
+ my ( $params ) = @_;
+
+ $params->{model} ||= $ENV{IO_ASYNC_ROUTINE_MODEL} || PREFERRED_MODEL;
+
+ $self->SUPER::_init( @_ );
+}
+
+sub configure
+{
+ my $self = shift;
+ my %params = @_;
+
+ # TODO: Can only reconfigure when not running
+ foreach (qw( channels_in channels_out code setup on_finish on_return on_die )) {
+ $self->{$_} = delete $params{$_} if exists $params{$_};
+ }
+
+ if( defined( my $model = delete $params{model} ) ) {
+ $model eq "fork" or $model eq "thread" or
+ croak "Expected 'model' to be either 'fork' or 'thread'";
+
+ $model eq "fork" and !IO::Async::OS->HAVE_POSIX_FORK and
+ croak "Cannot use 'fork' model as fork() is not available";
+ $model eq "thread" and !IO::Async::OS->HAVE_THREADS and
+ croak "Cannot use 'thread' model as threads are not available";
+
+ $self->{model} = $model;
+ }
+
+ $self->SUPER::configure( %params );
+}
+
+sub _add_to_loop
+{
+ my $self = shift;
+ my ( $loop ) = @_;
+ $self->SUPER::_add_to_loop( $loop );
+
+ return $self->_setup_fork if $self->{model} eq "fork";
+ return $self->_setup_thread if $self->{model} eq "thread";
+
+ die "TODO: unrecognised Routine model $self->{model}";
+}
+
+sub _setup_fork
+{
+ my $self = shift;
+
+ my @setup;
+ my @channels_in;
+ my @channels_out;
+
+ foreach my $ch ( @{ $self->{channels_in} || [] } ) {
+ my ( $rd, $wr );
+ unless( $rd = $ch->_extract_read_handle ) {
+ ( $rd, $wr ) = IO::Async::OS->pipepair;
+ }
+ push @setup, $rd => "keep";
+ push @channels_in, [ $ch, $wr, $rd ];
+ }
+
+ foreach my $ch ( @{ $self->{channels_out} || [] } ) {
+ my ( $rd, $wr );
+ unless( $wr = $ch->_extract_write_handle ) {
+ ( $rd, $wr ) = IO::Async::OS->pipepair;
+ }
+ push @setup, $wr => "keep";
+ push @channels_out, [ $ch, $rd, $wr ];
+ }
+
+ my $code = $self->{code};
+
+ my $setup = $self->{setup};
+ push @setup, @$setup if $setup;
+
+ my $process = IO::Async::Process->new(
+ setup => \@setup,
+ code => sub {
+ foreach ( @channels_in ) {
+ my ( $ch, undef, $rd ) = @$_;
+ $ch->setup_sync_mode( $rd );
+ }
+ foreach ( @channels_out ) {
+ my ( $ch, undef, $wr ) = @$_;
+ $ch->setup_sync_mode( $wr );
+ }
+
+ my $ret = $code->();
+
+ foreach ( @channels_in, @channels_out ) {
+ my ( $ch ) = @$_;
+ $ch->close;
+ }
+
+ return $ret;
+ },
+ on_finish => $self->_replace_weakself( sub {
+ my $self = shift or return;
+ my ( $exitcode ) = @_;
+ $self->maybe_invoke_event( on_finish => $exitcode );
+
+ $self->maybe_invoke_event( on_return => ($exitcode >> 8) ) unless $exitcode & 0x7f;
+ }),
+ on_exception => $self->_replace_weakself( sub {
+ my $self = shift or return;
+ my ( $exception, $errno, $exitcode ) = @_;
+
+ $self->maybe_invoke_event( on_die => $exception );
+ }),
+ );
+
+ foreach ( @channels_in ) {
+ my ( $ch, $wr ) = @$_;
+
+ $ch->setup_async_mode( write_handle => $wr );
+
+ $self->add_child( $ch ) unless $ch->parent;
+ }
+
+ foreach ( @channels_out ) {
+ my ( $ch, $rd ) = @$_;
+
+ $ch->setup_async_mode( read_handle => $rd );
+
+ $self->add_child( $ch ) unless $ch->parent;
+ }
+
+ $self->add_child( $self->{process} = $process );
+ $self->{id} = "P" . $process->pid;
+
+ foreach ( @channels_in, @channels_out ) {
+ my ( undef, undef, $other ) = @$_;
+ $other->close;
+ }
+}
+
+sub _setup_thread
+{
+ my $self = shift;
+
+ my @channels_in;
+ my @channels_out;
+
+ foreach my $ch ( @{ $self->{channels_in} || [] } ) {
+ my ( $rd, $wr );
+ unless( $rd = $ch->_extract_read_handle ) {
+ ( $rd, $wr ) = IO::Async::OS->pipepair;
+ }
+ push @channels_in, [ $ch, $wr, $rd ];
+ }
+
+ foreach my $ch ( @{ $self->{channels_out} || [] } ) {
+ my ( $rd, $wr );
+ unless( $wr = $ch->_extract_write_handle ) {
+ ( $rd, $wr ) = IO::Async::OS->pipepair;
+ }
+ push @channels_out, [ $ch, $rd, $wr ];
+ }
+
+ my $code = $self->{code};
+
+ my $tid = $self->loop->create_thread(
+ code => sub {
+ foreach ( @channels_in ) {
+ my ( $ch, $wr, $rd ) = @$_;
+ $ch->setup_sync_mode( $rd );
+ $wr->close if $wr;
+ }
+ foreach ( @channels_out ) {
+ my ( $ch, $rd, $wr ) = @$_;
+ $ch->setup_sync_mode( $wr );
+ $rd->close if $rd;
+ }
+
+ my $ret = $code->();
+
+ foreach ( @channels_in, @channels_out ) {
+ my ( $ch ) = @$_;
+ $ch->close;
+ }
+
+ return $ret;
+ },
+ on_joined => $self->_capture_weakself( sub {
+ my $self = shift or return;
+ my ( $ev, @result ) = @_;
+ $self->maybe_invoke_event( on_finish => @_ );
+
+ $self->maybe_invoke_event( on_return => @result ) if $ev eq "return";
+ $self->maybe_invoke_event( on_die => $result[0] ) if $ev eq "died";
+
+ delete $self->{tid};
+ }),
+ );
+
+ $self->{tid} = $tid;
+ $self->{id} = "T" . $tid;
+
+ foreach ( @channels_in ) {
+ my ( $ch, $wr, $rd ) = @$_;
+
+ $ch->setup_async_mode( write_handle => $wr );
+ $rd->close;
+
+ $self->add_child( $ch ) unless $ch->parent;
+ }
+
+ foreach ( @channels_out ) {
+ my ( $ch, $rd, $wr ) = @$_;
+
+ $ch->setup_async_mode( read_handle => $rd );
+ $wr->close;
+
+ $self->add_child( $ch ) unless $ch->parent;
+ }
+}
+
+=head1 METHODS
+
+=cut
+
+=head2 $id = $routine->id
+
+Returns an ID string that uniquely identifies the Routine out of all the
+currently-running ones. (The ID of already-exited Routines may be reused,
+however.)
+
+=cut
+
+sub id
+{
+ my $self = shift;
+ return $self->{id};
+}
+
+=head2 $model = $routine->model
+
+Returns the detachment model in use by the Routine.
+
+=cut
+
+sub model
+{
+ my $self = shift;
+ return $self->{model};
+}
+
+=head2 $routine->kill( $signal )
+
+Sends the specified signal to the routine code. This is either implemented by
+C<CORE::kill()> or C<threads::kill> as required. Note that in the thread case
+this has the usual limits of signal delivery to threads; namely, that it works
+at the Perl interpreter level, and cannot actually interrupt blocking system
+calls.
+
+=cut
+
+sub kill
+{
+ my $self = shift;
+ my ( $signal ) = @_;
+
+ $self->{process}->kill( $signal ) if $self->{model} eq "fork";
+ threads->object( $self->{tid} )->kill( $signal ) if $self->{model} eq "thread";
+}
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/Signal.pm b/lib/IO/Async/Signal.pm
new file mode 100644
index 0000000..4ef68f5
--- /dev/null
+++ b/lib/IO/Async/Signal.pm
@@ -0,0 +1,150 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2009-2011 -- leonerd@leonerd.org.uk
+
+package IO::Async::Signal;
+
+use strict;
+use warnings;
+use base qw( IO::Async::Notifier );
+
+our $VERSION = '0.67';
+
+use Carp;
+
+=head1 NAME
+
+C<IO::Async::Signal> - event callback on receipt of a POSIX signal
+
+=head1 SYNOPSIS
+
+ use IO::Async::Signal;
+
+ use IO::Async::Loop;
+ my $loop = IO::Async::Loop->new;
+
+ my $signal = IO::Async::Signal->new(
+ name => "HUP",
+
+ on_receipt => sub {
+ print "I caught SIGHUP\n";
+ },
+ );
+
+ $loop->add( $signal );
+
+ $loop->run;
+
+=head1 DESCRIPTION
+
+This subclass of L<IO::Async::Notifier> invokes its callback when a particular
+POSIX signal is received.
+
+Multiple objects can be added to a C<Loop> that all watch for the same signal.
+The callback functions will all be invoked, in no particular order.
+
+=cut
+
+=head1 EVENTS
+
+The following events are invoked, either using subclass methods or CODE
+references in parameters:
+
+=head2 on_receipt
+
+Invoked when the signal is received.
+
+=cut
+
+=head1 PARAMETERS
+
+The following named parameters may be passed to C<new> or C<configure>:
+
+=head2 name => STRING
+
+The name of the signal to watch. This should be a bare name like C<TERM>. Can
+only be given at construction time.
+
+=head2 on_receipt => CODE
+
+CODE reference for the C<on_receipt> event.
+
+Once constructed, the C<Signal> will need to be added to the C<Loop> before it
+will work.
+
+=cut
+
+sub _init
+{
+ my $self = shift;
+ my ( $params ) = @_;
+
+ my $name = delete $params->{name} or croak "Expected 'name'";
+
+ $name =~ s/^SIG//; # Trim a leading "SIG"
+
+ $self->{name} = $name;
+
+ $self->SUPER::_init( $params );
+}
+
+sub configure
+{
+ my $self = shift;
+ my %params = @_;
+
+ if( exists $params{on_receipt} ) {
+ $self->{on_receipt} = delete $params{on_receipt};
+
+ undef $self->{cb}; # Will be lazily constructed when needed
+
+ if( my $loop = $self->loop ) {
+ $self->_remove_from_loop( $loop );
+ $self->_add_to_loop( $loop );
+ }
+ }
+
+ unless( $self->can_event( 'on_receipt' ) ) {
+ croak 'Expected either a on_receipt callback or an ->on_receipt method';
+ }
+
+ $self->SUPER::configure( %params );
+}
+
+sub _add_to_loop
+{
+ my $self = shift;
+ my ( $loop ) = @_;
+
+ $self->{cb} ||= $self->make_event_cb( 'on_receipt' );
+
+ $self->{id} = $loop->attach_signal( $self->{name}, $self->{cb} );
+}
+
+sub _remove_from_loop
+{
+ my $self = shift;
+ my ( $loop ) = @_;
+
+ $loop->detach_signal( $self->{name}, $self->{id} );
+ undef $self->{id};
+}
+
+sub notifier_name
+{
+ my $self = shift;
+ if( length( my $name = $self->SUPER::notifier_name ) ) {
+ return $name;
+ }
+
+ return $self->{name};
+}
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/Socket.pm b/lib/IO/Async/Socket.pm
new file mode 100644
index 0000000..23a4973
--- /dev/null
+++ b/lib/IO/Async/Socket.pm
@@ -0,0 +1,358 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2011-2015 -- leonerd@leonerd.org.uk
+
+package IO::Async::Socket;
+
+use strict;
+use warnings;
+
+our $VERSION = '0.67';
+
+use base qw( IO::Async::Handle );
+
+use Errno qw( EAGAIN EWOULDBLOCK EINTR );
+
+use Carp;
+
+=head1 NAME
+
+C<IO::Async::Socket> - event callbacks and send buffering for a socket
+filehandle
+
+=head1 SYNOPSIS
+
+ use IO::Async::Socket;
+
+ use IO::Async::Loop;
+ my $loop = IO::Async::Loop->new;
+
+ my $socket = IO::Async::Socket->new(
+ on_recv => sub {
+ my ( $self, $dgram, $addr ) = @_;
+
+ print "Received reply: $dgram\n",
+ $loop->stop;
+ },
+ on_recv_error => sub {
+ my ( $self, $errno ) = @_;
+ die "Cannot recv - $errno\n";
+ },
+ );
+ $loop->add( $socket );
+
+ $socket->connect(
+ host => "some.host.here",
+ service => "echo",
+ socktype => 'dgram',
+ )->get;
+
+ $socket->send( "A TEST DATAGRAM" );
+
+ $loop->run;
+
+=head1 DESCRIPTION
+
+This subclass of L<IO::Async::Handle> contains a socket filehandle. It
+provides a queue of outgoing data. It invokes the C<on_recv> handler when new
+data is received from the filehandle. Data may be sent to the filehandle by
+calling the C<send> method.
+
+It is primarily intended for C<SOCK_DGRAM> or C<SOCK_RAW> sockets (such as UDP
+or packet-capture); for C<SOCK_STREAM> sockets (such as TCP) an instance of
+L<IO::Async::Stream> is more appropriate.
+
+=head1 EVENTS
+
+The following events are invoked, either using subclass methods or CODE
+references in parameters:
+
+=head2 on_recv $data, $addr
+
+Invoke on receipt of a packet, datagram, or stream segment.
+
+The C<on_recv> handler is invoked once for each packet, datagram, or stream
+segment that is received. It is passed the data itself, and the sender's
+address.
+
+=head2 on_recv_error $errno
+
+Optional. Invoked when the C<recv> method on the receiving handle fails.
+
+=head2 on_send_error $errno
+
+Optional. Invoked when the C<send> method on the sending handle fails.
+
+The C<on_recv_error> and C<on_send_error> handlers are passed the value of
+C<$!> at the time the error occured. (The C<$!> variable itself, by its
+nature, may have changed from the original error by the time this handler
+runs so it should always use the value passed in).
+
+If an error occurs when the corresponding error callback is not supplied, and
+there is not a subclass method for it, then the C<close> method is
+called instead.
+
+=head2 on_outgoing_empty
+
+Optional. Invoked when the sending data buffer becomes empty.
+
+=cut
+
+sub _init
+{
+ my $self = shift;
+
+ $self->{recv_len} = 65536;
+
+ $self->SUPER::_init( @_ );
+}
+
+=head1 PARAMETERS
+
+The following named parameters may be passed to C<new> or C<configure>:
+
+=head2 read_handle => IO
+
+The IO handle to receive from. Must implement C<fileno> and C<recv> methods.
+
+=head2 write_handle => IO
+
+The IO handle to send to. Must implement C<fileno> and C<send> methods.
+
+=head2 handle => IO
+
+Shortcut to specifying the same IO handle for both of the above.
+
+=head2 on_recv => CODE
+
+=head2 on_recv_error => CODE
+
+=head2 on_outgoing_empty => CODE
+
+=head2 on_send_error => CODE
+
+=head2 autoflush => BOOL
+
+Optional. If true, the C<send> method will atempt to send data to the
+operating system immediately, without waiting for the loop to indicate the
+filehandle is write-ready.
+
+=head2 recv_len => INT
+
+Optional. Sets the buffer size for C<recv> calls. Defaults to 64 KiB.
+
+=head2 recv_all => BOOL
+
+Optional. If true, repeatedly call C<recv> when the receiving handle first
+becomes read-ready. By default this is turned off, meaning at most one
+fixed-size buffer is received. If there is still more data in the kernel's
+buffer, the handle will stil be readable, and will be received from again.
+
+This behaviour allows multiple streams and sockets to be multiplexed
+simultaneously, meaning that a large bulk transfer on one cannot starve other
+filehandles of processing time. Turning this option on may improve bulk data
+transfer rate, at the risk of delaying or stalling processing on other
+filehandles.
+
+=head2 send_all => INT
+
+Optional. Analogous to the C<recv_all> option, but for sending. When
+C<autoflush> is enabled, this option only affects deferred sending if the
+initial attempt failed.
+
+The condition requiring an C<on_recv> handler is checked at the time the
+object is added to a Loop; it is allowed to create a C<IO::Async::Socket>
+object with a read handle but without a C<on_recv> handler, provided that
+one is later given using C<configure> before the stream is added to its
+containing Loop, either directly or by being a child of another Notifier
+already in a Loop, or added to one.
+
+=cut
+
+sub configure
+{
+ my $self = shift;
+ my %params = @_;
+
+ for (qw( on_recv on_outgoing_empty on_recv_error on_send_error
+ recv_len recv_all send_all autoflush )) {
+ $self->{$_} = delete $params{$_} if exists $params{$_};
+ }
+
+ $self->SUPER::configure( %params );
+
+ if( $self->loop and defined $self->read_handle ) {
+ $self->can_event( "on_recv" ) or
+ croak 'Expected either an on_recv callback or to be able to ->on_recv';
+ }
+}
+
+sub _add_to_loop
+{
+ my $self = shift;
+
+ if( defined $self->read_handle ) {
+ $self->can_event( "on_recv" ) or
+ croak 'Expected either an on_recv callback or to be able to ->on_recv';
+ }
+
+ $self->SUPER::_add_to_loop( @_ );
+}
+
+=head1 METHODS
+
+=cut
+
+=head2 $socket->send( $data, $flags, $addr )
+
+This method adds a segment of data to be sent, or sends it immediately,
+according to the C<autoflush> parameter. C<$flags> and C<$addr> are optional.
+
+If the C<autoflush> option is set, this method will try immediately to send
+the data to the underlying filehandle, optionally using the given flags and
+destination address. If this completes successfully then it will have been
+sent by the time this method returns. If it fails to send, then the data is
+queued as if C<autoflush> were not set, and will be flushed as normal.
+
+=cut
+
+sub send
+{
+ my $self = shift;
+ my ( $data, $flags, $addr ) = @_;
+
+ croak "Cannot send data to a Socket with no write_handle" unless my $handle = $self->write_handle;
+
+ my $sendqueue = $self->{sendqueue} ||= [];
+ push @$sendqueue, [ $data, $flags, $addr ];
+
+ if( $self->{autoflush} ) {
+ while( @$sendqueue ) {
+ my ( $data, $flags, $addr ) = @{ $sendqueue->[0] };
+ my $len = $handle->send( $data, $flags, $addr );
+
+ last if !$len; # stop on any errors and defer back to the non-autoflush path
+
+ shift @$sendqueue;
+ }
+
+ if( !@$sendqueue ) {
+ $self->want_writeready( 0 );
+ return;
+ }
+ }
+
+ $self->want_writeready( 1 );
+}
+
+sub on_read_ready
+{
+ my $self = shift;
+
+ my $handle = $self->read_handle;
+
+ while(1) {
+ my $addr = $handle->recv( my $data, $self->{recv_len} );
+
+ if( !defined $addr ) {
+ return if $! == EAGAIN || $! == EWOULDBLOCK || $! == EINTR;
+
+ my $errno = $!;
+
+ $self->maybe_invoke_event( on_recv_error => $errno )
+ or $self->close;
+
+ return;
+ }
+
+ if( !length $data ) {
+ $self->close;
+ return;
+ }
+
+ $self->invoke_event( on_recv => $data, $addr );
+
+ last unless $self->{recv_all};
+ }
+}
+
+sub on_write_ready
+{
+ my $self = shift;
+
+ my $handle = $self->write_handle;
+
+ my $sendqueue = $self->{sendqueue};
+
+ while( $sendqueue and @$sendqueue ) {
+ my ( $data, $flags, $addr ) = @{ shift @$sendqueue };
+ my $len = $handle->send( $data, $flags, $addr );
+
+ if( !defined $len ) {
+ return if $! == EAGAIN || $! == EWOULDBLOCK || $! == EINTR;
+
+ my $errno = $!;
+
+ $self->maybe_invoke_event( on_send_error => $errno )
+ or $self->close;
+
+ return;
+ }
+
+ if( $len == 0 ) {
+ $self->close;
+ return;
+ }
+
+ last unless $self->{send_all};
+ }
+
+ if( !$sendqueue or !@$sendqueue ) {
+ $self->want_writeready( 0 );
+
+ $self->maybe_invoke_event( on_outgoing_empty => );
+ }
+}
+
+=head1 EXAMPLES
+
+=head2 Send-first on a UDP Socket
+
+C<UDP> is carried by the C<SOCK_DGRAM> socket type, for which the string
+C<'dgram'> is a convenient shortcut:
+
+ $socket->connect(
+ host => $hostname,
+ service => $service,
+ socktype => 'dgram',
+ ...
+ )
+
+=head2 Receive-first on a UDP Socket
+
+A typical server pattern with C<UDP> involves binding a well-known port
+number instead of connecting to one, and waiting on incoming packets.
+
+ $socket->bind(
+ service => 12345,
+ socktype => 'dgram',
+ )->get;
+
+=head1 SEE ALSO
+
+=over 4
+
+=item *
+
+L<IO::Handle> - Supply object methods for I/O handles
+
+=back
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/Stream.pm b/lib/IO/Async/Stream.pm
new file mode 100644
index 0000000..487eb38
--- /dev/null
+++ b/lib/IO/Async/Stream.pm
@@ -0,0 +1,1419 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2006-2015 -- leonerd@leonerd.org.uk
+
+package IO::Async::Stream;
+
+use strict;
+use warnings;
+use 5.010; # //
+
+our $VERSION = '0.67';
+
+use base qw( IO::Async::Handle );
+
+use Errno qw( EAGAIN EWOULDBLOCK EINTR EPIPE );
+
+use Carp;
+
+use Encode 2.11 qw( find_encoding STOP_AT_PARTIAL );
+use Scalar::Util qw( blessed );
+
+use IO::Async::Debug;
+
+# Tuneable from outside
+# Not yet documented
+our $READLEN = 8192;
+our $WRITELEN = 8192;
+
+use Struct::Dumb;
+
+# Element of the writequeue
+struct Writer => [qw( data writelen on_write on_flush on_error watching )];
+
+# Element of the readqueue
+struct Reader => [qw( on_read future )];
+
+# Bitfields in the want flags
+use constant WANT_READ_FOR_READ => 0x01;
+use constant WANT_READ_FOR_WRITE => 0x02;
+use constant WANT_WRITE_FOR_READ => 0x04;
+use constant WANT_WRITE_FOR_WRITE => 0x08;
+use constant WANT_ANY_READ => WANT_READ_FOR_READ |WANT_READ_FOR_WRITE;
+use constant WANT_ANY_WRITE => WANT_WRITE_FOR_READ|WANT_WRITE_FOR_WRITE;
+
+=head1 NAME
+
+C<IO::Async::Stream> - event callbacks and write bufering for a stream
+filehandle
+
+=head1 SYNOPSIS
+
+ use IO::Async::Stream;
+
+ use IO::Async::Loop;
+ my $loop = IO::Async::Loop->new;
+
+ my $stream = IO::Async::Stream->new(
+ read_handle => \*STDIN,
+ write_handle => \*STDOUT,
+
+ on_read => sub {
+ my ( $self, $buffref, $eof ) = @_;
+
+ while( $$buffref =~ s/^(.*\n)// ) {
+ print "Received a line $1";
+ }
+
+ if( $eof ) {
+ print "EOF; last partial line is $$buffref\n";
+ }
+
+ return 0;
+ }
+ );
+
+ $loop->add( $stream );
+
+ $stream->write( "An initial line here\n" );
+
+=head1 DESCRIPTION
+
+This subclass of L<IO::Async::Handle> contains a filehandle that represents
+a byte-stream. It provides buffering for both incoming and outgoing data. It
+invokes the C<on_read> handler when new data is read from the filehandle. Data
+may be written to the filehandle by calling the C<write> method.
+
+This class is suitable for any kind of filehandle that provides a
+possibly-bidirectional reliable byte stream, such as a pipe, TTY, or
+C<SOCK_STREAM> socket (such as TCP or a byte-oriented UNIX local socket). For
+datagram or raw message-based sockets (such as UDP) see instead
+L<IO::Async::Socket>.
+
+=cut
+
+=head1 EVENTS
+
+The following events are invoked, either using subclass methods or CODE
+references in parameters:
+
+=head2 $ret = on_read \$buffer, $eof
+
+Invoked when more data is available in the internal receiving buffer.
+
+The first argument is a reference to a plain perl string. The code should
+inspect and remove any data it likes, but is not required to remove all, or
+indeed any of the data. Any data remaining in the buffer will be preserved for
+the next call, the next time more data is received from the handle.
+
+In this way, it is easy to implement code that reads records of some form when
+completed, but ignores partially-received records, until all the data is
+present. If the handler is confident no more useful data remains, it should
+return C<0>. If not, it should return C<1>, and the handler will be called
+again. This makes it easy to implement code that handles multiple incoming
+records at the same time. See the examples at the end of this documentation
+for more detail.
+
+The second argument is a scalar indicating whether the stream has reported an
+end-of-file (EOF) condition. A reference to the buffer is passed to the
+handler in the usual way, so it may inspect data contained in it. Once the
+handler returns a false value, it will not be called again, as the handle is
+now at EOF and no more data can arrive.
+
+The C<on_read> code may also dynamically replace itself with a new callback
+by returning a CODE reference instead of C<0> or C<1>. The original callback
+or method that the object first started with may be restored by returning
+C<undef>. Whenever the callback is changed in this way, the new code is called
+again; even if the read buffer is currently empty. See the examples at the end
+of this documentation for more detail.
+
+The C<push_on_read> method can be used to insert new, temporary handlers that
+take precedence over the global C<on_read> handler. This event is only used if
+there are no further pending handlers created by C<push_on_read>.
+
+=head2 on_read_eof
+
+Optional. Invoked when the read handle indicates an end-of-file (EOF)
+condition. If there is any data in the buffer still to be processed, the
+C<on_read> event will be invoked first, before this one.
+
+=head2 on_write_eof
+
+Optional. Invoked when the write handle indicates an end-of-file (EOF)
+condition. Note that this condition can only be detected after a C<write>
+syscall returns the C<EPIPE> error. If there is no data pending to be written
+then it will not be detected yet.
+
+=head2 on_read_error $errno
+
+Optional. Invoked when the C<sysread> method on the read handle fails.
+
+=head2 on_write_error $errno
+
+Optional. Invoked when the C<syswrite> method on the write handle fails.
+
+The C<on_read_error> and C<on_write_error> handlers are passed the value of
+C<$!> at the time the error occured. (The C<$!> variable itself, by its
+nature, may have changed from the original error by the time this handler
+runs so it should always use the value passed in).
+
+If an error occurs when the corresponding error callback is not supplied, and
+there is not a handler for it, then the C<close> method is called instead.
+
+=head2 on_read_high_watermark $length
+
+=head2 on_read_low_watermark $length
+
+Optional. Invoked when the read buffer grows larger than the high watermark
+or smaller than the low watermark respectively. These are edge-triggered
+events; they will only be triggered once per crossing, not continuously while
+the buffer remains above or below the given limit.
+
+If these event handlers are not defined, the default behaviour is to disable
+read-ready notifications if the read buffer grows larger than the high
+watermark (so as to avoid it growing arbitrarily if nothing is consuming it),
+and re-enable notifications again once something has read enough to cause it to
+drop. If these events are overridden, the overriding code will have to perform
+this behaviour if required, by using
+
+ $self->want_readready_for_read(...)
+
+=head2 on_outgoing_empty
+
+Optional. Invoked when the writing data buffer becomes empty.
+
+=head2 on_writeable_start
+
+=head2 on_writeable_stop
+
+Optional. These two events inform when the filehandle becomes writeable, and
+when it stops being writeable. C<on_writeable_start> is invoked by the
+C<on_write_ready> event if previously it was known to be not writeable.
+C<on_writeable_stop> is invoked after a C<syswrite> operation fails with
+C<EAGAIN> or C<EWOULDBLOCK>. These two events track the writeability state,
+and ensure that only state change cause events to be invoked. A stream starts
+off being presumed writeable, so the first of these events to be observed will
+be C<on_writeable_stop>.
+
+=cut
+
+sub _init
+{
+ my $self = shift;
+
+ $self->{writequeue} = []; # Queue of Writers
+ $self->{readqueue} = []; # Queue of Readers
+ $self->{writeable} = 1; # "innocent until proven guilty" (by means of EAGAIN)
+ $self->{readbuff} = "";
+
+ $self->{reader} = "_sysread";
+ $self->{writer} = "_syswrite";
+
+ $self->{read_len} = $READLEN;
+ $self->{write_len} = $WRITELEN;
+
+ $self->{want} = WANT_READ_FOR_READ;
+
+ $self->{close_on_read_eof} = 1;
+}
+
+=head1 PARAMETERS
+
+The following named parameters may be passed to C<new> or C<configure>:
+
+=head2 read_handle => IO
+
+The IO handle to read from. Must implement C<fileno> and C<sysread> methods.
+
+=head2 write_handle => IO
+
+The IO handle to write to. Must implement C<fileno> and C<syswrite> methods.
+
+=head2 handle => IO
+
+Shortcut to specifying the same IO handle for both of the above.
+
+=head2 on_read => CODE
+
+=head2 on_read_error => CODE
+
+=head2 on_outgoing_empty => CODE
+
+=head2 on_write_error => CODE
+
+=head2 on_writeable_start => CODE
+
+=head2 on_writeable_stop => CODE
+
+CODE references for event handlers.
+
+=head2 autoflush => BOOL
+
+Optional. If true, the C<write> method will attempt to write data to the
+operating system immediately, without waiting for the loop to indicate the
+filehandle is write-ready. This is useful, for example, on streams that should
+contain up-to-date logging or console information.
+
+It currently defaults to false for any file handle, but future versions of
+C<IO::Async> may enable this by default on STDOUT and STDERR.
+
+=head2 read_len => INT
+
+Optional. Sets the buffer size for C<read> calls. Defaults to 8 KiBytes.
+
+=head2 read_all => BOOL
+
+Optional. If true, attempt to read as much data from the kernel as possible
+when the handle becomes readable. By default this is turned off, meaning at
+most one fixed-size buffer is read. If there is still more data in the
+kernel's buffer, the handle will still be readable, and will be read from
+again.
+
+This behaviour allows multiple streams and sockets to be multiplexed
+simultaneously, meaning that a large bulk transfer on one cannot starve other
+filehandles of processing time. Turning this option on may improve bulk data
+transfer rate, at the risk of delaying or stalling processing on other
+filehandles.
+
+=head2 write_len => INT
+
+Optional. Sets the buffer size for C<write> calls. Defaults to 8 KiBytes.
+
+=head2 write_all => BOOL
+
+Optional. Analogous to the C<read_all> option, but for writing. When
+C<autoflush> is enabled, this option only affects deferred writing if the
+initial attempt failed due to buffer space.
+
+=head2 read_high_watermark => INT
+
+=head2 read_low_watermark => INT
+
+Optional. If defined, gives a way to implement flow control or other
+behaviours that depend on the size of Stream's read buffer.
+
+If after more data is read from the underlying filehandle the read buffer is
+now larger than the high watermark, the C<on_read_high_watermark> event is
+triggered (which, by default, will disable read-ready notifications and pause
+reading from the filehandle).
+
+If after data is consumed by an C<on_read> handler the read buffer is now
+smaller than the low watermark, the C<on_read_low_watermark> event is
+triggered (which, by default, will re-enable read-ready notifications and
+resume reading from the filehandle). For to be possible, the read handler
+would have to be one added by the C<push_on_read> method or one of the
+Future-returning C<read_*> methods.
+
+By default these options are not defined, so this behaviour will not happen.
+C<read_low_watermark> may not be set to a larger value than
+C<read_high_watermark>, but it may be set to a smaller value, creating a
+hysteresis region. If either option is defined then both must be.
+
+If these options are used with the default event handlers, be careful not to
+cause deadlocks by having a high watermark sufficiently low that a single
+C<on_read> invocation might not consider it finished yet.
+
+=head2 reader => STRING|CODE
+
+=head2 writer => STRING|CODE
+
+Optional. If defined, gives the name of a method or a CODE reference to use
+to implement the actual reading from or writing to the filehandle. These will
+be invoked as
+
+ $stream->reader( $read_handle, $buffer, $len )
+ $stream->writer( $write_handle, $buffer, $len )
+
+Each is expected to modify the passed buffer; C<reader> by appending to it,
+C<writer> by removing a prefix from it. Each is expected to return a true
+value on success, zero on EOF, or C<undef> with C<$!> set for errors. If not
+provided, they will be substituted by implenentations using C<sysread> and
+C<syswrite> on the underlying handle, respectively.
+
+=head2 close_on_read_eof => BOOL
+
+Optional. Usually true, but if set to a false value then the stream will not
+be C<close>d when an EOF condition occurs on read. This is normally not useful
+as at that point the underlying stream filehandle is no longer useable, but it
+may be useful for reading regular files, or interacting with TTY devices.
+
+=head2 encoding => STRING
+
+If supplied, sets the name of encoding of the underlying stream. If an
+encoding is set, then the C<write> method will expect to receive Unicode
+strings and encodes them into bytes, and incoming bytes will be decoded into
+Unicode strings for the C<on_read> event.
+
+If an encoding is not supplied then C<write> and C<on_read> will work in byte
+strings.
+
+I<IMPORTANT NOTE:> in order to handle reads of UTF-8 content or other
+multibyte encodings, the code implementing the C<on_read> event uses a feature
+of L<Encode>; the C<STOP_AT_PARTIAL> flag. While this flag has existed for a
+while and is used by the C<:encoding> PerlIO layer itself for similar
+purposes, the flag is not officially documented by the C<Encode> module. In
+principle this undocumented feature could be subject to change, in practice I
+believe it to be reasonably stable.
+
+This note applies only to the C<on_read> event; data written using the
+C<write> method does not rely on any undocumented features of C<Encode>.
+
+If a read handle is given, it is required that either an C<on_read> callback
+reference is configured, or that the object provides an C<on_read> method. It
+is optional whether either is true for C<on_outgoing_empty>; if neither is
+supplied then no action will be taken when the writing buffer becomes empty.
+
+An C<on_read> handler may be supplied even if no read handle is yet given, to
+be used when a read handle is eventually provided by the C<set_handles>
+method.
+
+This condition is checked at the time the object is added to a Loop; it is
+allowed to create a C<IO::Async::Stream> object with a read handle but without
+a C<on_read> handler, provided that one is later given using C<configure>
+before the stream is added to its containing Loop, either directly or by being
+a child of another Notifier already in a Loop, or added to one.
+
+=cut
+
+sub configure
+{
+ my $self = shift;
+ my %params = @_;
+
+ for (qw( on_read on_outgoing_empty on_read_eof on_write_eof on_read_error
+ on_write_error on_writeable_start on_writeable_stop autoflush
+ read_len read_all write_len write_all on_read_high_watermark
+ on_read_low_watermark reader writer close_on_read_eof )) {
+ $self->{$_} = delete $params{$_} if exists $params{$_};
+ }
+
+ if( exists $params{read_high_watermark} or exists $params{read_low_watermark} ) {
+ my $high = delete $params{read_high_watermark} // $self->{read_high_watermark};
+ my $low = delete $params{read_low_watermark} // $self->{read_low_watermark};
+
+ croak "Cannot set read_low_watermark without read_high_watermark" if defined $low and !defined $high;
+ croak "Cannot set read_high_watermark without read_low_watermark" if defined $high and !defined $low;
+
+ croak "Cannot set read_low_watermark higher than read_high_watermark" if defined $low and defined $high and $low > $high;
+
+ $self->{read_high_watermark} = $high;
+ $self->{read_low_watermark} = $low;
+
+ # TODO: reassert levels if we've moved them
+ }
+
+ if( exists $params{encoding} ) {
+ my $encoding = delete $params{encoding};
+ my $obj = find_encoding( $encoding );
+ defined $obj or croak "Cannot handle an encoding of '$encoding'";
+ $self->{encoding} = $obj;
+ }
+
+ $self->SUPER::configure( %params );
+
+ if( $self->loop and $self->read_handle ) {
+ $self->can_event( "on_read" ) or
+ croak 'Expected either an on_read callback or to be able to ->on_read';
+ }
+}
+
+sub _add_to_loop
+{
+ my $self = shift;
+
+ if( defined $self->read_handle ) {
+ $self->can_event( "on_read" ) or
+ croak 'Expected either an on_read callback or to be able to ->on_read';
+ }
+
+ $self->SUPER::_add_to_loop( @_ );
+
+ if( !$self->_is_empty ) {
+ $self->want_writeready_for_write( 1 );
+ }
+}
+
+=head1 METHODS
+
+The following methods documented with a trailing call to C<< ->get >> return
+L<Future> instances.
+
+=cut
+
+=head2 $stream->want_readready_for_read( $set )
+
+=head2 $stream->want_readready_for_write( $set )
+
+Mutators for the C<want_readready> property on L<IO::Async::Handle>, which
+control whether the C<read> or C<write> behaviour should be continued once the
+filehandle becomes ready for read.
+
+Normally, C<want_readready_for_read> is always true (though the read watermark
+behaviour can modify it), and C<want_readready_for_write> is not used.
+However, if a custom C<writer> function is provided, it may find this useful
+for being invoked again if it cannot proceed with a write operation until the
+filehandle becomes readable (such as during transport negotiation or SSL key
+management, for example).
+
+=cut
+
+sub want_readready_for_read
+{
+ my $self = shift;
+ my ( $set ) = @_;
+ $set ? ( $self->{want} |= WANT_READ_FOR_READ ) : ( $self->{want} &= ~WANT_READ_FOR_READ );
+
+ $self->want_readready( $self->{want} & WANT_ANY_READ ) if $self->read_handle;
+}
+
+sub want_readready_for_write
+{
+ my $self = shift;
+ my ( $set ) = @_;
+ $set ? ( $self->{want} |= WANT_READ_FOR_WRITE ) : ( $self->{want} &= ~WANT_READ_FOR_WRITE );
+
+ $self->want_readready( $self->{want} & WANT_ANY_READ ) if $self->read_handle;
+}
+
+=head2 $stream->want_writeready_for_write( $set )
+
+=head2 $stream->want_writeready_for_read( $set )
+
+Mutators for the C<want_writeready> property on L<IO::Async::Handle>, which
+control whether the C<write> or C<read> behaviour should be continued once the
+filehandle becomes ready for write.
+
+Normally, C<want_writeready_for_write> is managed by the C<write> method and
+associated flushing, and C<want_writeready_for_read> is not used. However, if
+a custom C<reader> function is provided, it may find this useful for being
+invoked again if it cannot proceed with a read operation until the filehandle
+becomes writable (such as during transport negotiation or SSL key management,
+for example).
+
+=cut
+
+sub want_writeready_for_write
+{
+ my $self = shift;
+ my ( $set ) = @_;
+ $set ? ( $self->{want} |= WANT_WRITE_FOR_WRITE ) : ( $self->{want} &= ~WANT_WRITE_FOR_WRITE );
+
+ $self->want_writeready( $self->{want} & WANT_ANY_WRITE ) if $self->write_handle;
+}
+
+sub want_writeready_for_read
+{
+ my $self = shift;
+ my ( $set ) = @_;
+ $set ? ( $self->{want} |= WANT_WRITE_FOR_READ ) : ( $self->{want} &= ~WANT_WRITE_FOR_READ );
+
+ $self->want_writeready( $self->{want} & WANT_ANY_WRITE ) if $self->write_handle;
+}
+
+# FUNCTION not method
+sub _nonfatal_error
+{
+ my ( $errno ) = @_;
+
+ return $errno == EAGAIN ||
+ $errno == EWOULDBLOCK ||
+ $errno == EINTR;
+}
+
+sub _is_empty
+{
+ my $self = shift;
+ return !@{ $self->{writequeue} };
+}
+
+=head2 $stream->close
+
+A synonym for C<close_when_empty>. This should not be used when the deferred
+wait behaviour is required, as the behaviour of C<close> may change in a
+future version of C<IO::Async>. Instead, call C<close_when_empty> directly.
+
+=cut
+
+sub close
+{
+ my $self = shift;
+ $self->close_when_empty;
+}
+
+=head2 $stream->close_when_empty
+
+If the write buffer is empty, this method calls C<close> on the underlying IO
+handles, and removes the stream from its containing loop. If the write buffer
+still contains data, then this is deferred until the buffer is empty. This is
+intended for "write-then-close" one-shot streams.
+
+ $stream->write( "Here is my final data\n" );
+ $stream->close_when_empty;
+
+Because of this deferred nature, it may not be suitable for error handling.
+See instead the C<close_now> method.
+
+=cut
+
+sub close_when_empty
+{
+ my $self = shift;
+
+ return $self->SUPER::close if $self->_is_empty;
+
+ $self->{stream_closing} = 1;
+}
+
+=head2 $stream->close_now
+
+This method immediately closes the underlying IO handles and removes the
+stream from the containing loop. It will not wait to flush the remaining data
+in the write buffer.
+
+=cut
+
+sub close_now
+{
+ my $self = shift;
+
+ foreach ( @{ $self->{writequeue} } ) {
+ $_->on_error->( "stream closing" ) if $_->on_error;
+ }
+
+ undef @{ $self->{writequeue} };
+ undef $self->{stream_closing};
+
+ $self->SUPER::close;
+}
+
+=head2 $eof = $stream->is_read_eof
+
+=head2 $eof = $stream->is_write_eof
+
+Returns true after an EOF condition is reported on either the read or the
+write handle, respectively.
+
+=cut
+
+sub is_read_eof
+{
+ my $self = shift;
+ return $self->{read_eof};
+}
+
+sub is_write_eof
+{
+ my $self = shift;
+ return $self->{write_eof};
+}
+
+=head2 $stream->write( $data, %params )
+
+This method adds data to the outgoing data queue, or writes it immediately,
+according to the C<autoflush> parameter.
+
+If the C<autoflush> option is set, this method will try immediately to write
+the data to the underlying filehandle. If this completes successfully then it
+will have been written by the time this method returns. If it fails to write
+completely, then the data is queued as if C<autoflush> were not set, and will
+be flushed as normal.
+
+C<$data> can either be a plain string, a L<Future>, or a CODE reference. If it
+is a plain string it is written immediately. If it is not, its value will be
+used to generate more C<$data> values, eventually leading to strings to be
+written.
+
+If C<$data> is a C<Future>, the Stream will wait until it is ready, and take
+the single value it yields.
+
+If C<$data> is a CODE reference, it will be repeatedly invoked to generate new
+values. Each time the filehandle is ready to write more data to it, the
+function is invoked. Once the function has finished generating data it should
+return undef. The function is passed the Stream object as its first argument.
+
+It is allowed that C<Future>s yield CODE references, or CODE references return
+C<Future>s, as well as plain strings.
+
+For example, to stream the contents of an existing opened filehandle:
+
+ open my $fileh, "<", $path or die "Cannot open $path - $!";
+
+ $stream->write( sub {
+ my ( $stream ) = @_;
+
+ sysread $fileh, my $buffer, 8192 or return;
+ return $buffer;
+ } );
+
+Takes the following optional named parameters in C<%params>:
+
+=over 8
+
+=item write_len => INT
+
+Overrides the C<write_len> parameter for the data written by this call.
+
+=item on_write => CODE
+
+A CODE reference which will be invoked after every successful C<syswrite>
+operation on the underlying filehandle. It will be passed the number of bytes
+that were written by this call, which may not be the entire length of the
+buffer - if it takes more than one C<syscall> operation to empty the buffer
+then this callback will be invoked multiple times.
+
+ $on_write->( $stream, $len )
+
+=item on_flush => CODE
+
+A CODE reference which will be invoked once the data queued by this C<write>
+call has been flushed. This will be invoked even if the buffer itself is not
+yet empty; if more data has been queued since the call.
+
+ $on_flush->( $stream )
+
+=item on_error => CODE
+
+A CODE reference which will be invoked if a C<syswrite> error happens while
+performing this write. Invoked as for the C<Stream>'s C<on_write_error> event.
+
+ $on_error->( $stream, $errno )
+
+=back
+
+If the object is not yet a member of a loop and doesn't yet have a
+C<write_handle>, then calls to the C<write> method will simply queue the data
+and return. It will be flushed when the object is added to the loop.
+
+If C<$data> is a defined but empty string, the write is still queued, and the
+C<on_flush> continuation will be invoked, if supplied. This can be used to
+obtain a marker, to invoke some code once the output queue has been flushed up
+to this point.
+
+=head2 $stream->write( ... )->get
+
+If called in non-void context, this method returns a L<Future> which will
+complete (with no value) when the write operation has been flushed. This may
+be used as an alternative to, or combined with, the C<on_flush> callback.
+
+=cut
+
+sub _syswrite
+{
+ my $self = shift;
+ my ( $handle, undef, $len ) = @_;
+
+ my $written = $handle->syswrite( $_[1], $len );
+ return $written if !$written; # zero or undef
+
+ substr( $_[1], 0, $written ) = "";
+ return $written;
+}
+
+sub _flush_one_write
+{
+ my $self = shift;
+
+ my $writequeue = $self->{writequeue};
+
+ my $head;
+ while( $head = $writequeue->[0] and ref $head->data ) {
+ if( ref $head->data eq "CODE" ) {
+ my $data = $head->data->( $self );
+ if( !defined $data ) {
+ $head->on_flush->( $self ) if $head->on_flush;
+ shift @$writequeue;
+ return 1;
+ }
+ if( !ref $data and my $encoding = $self->{encoding} ) {
+ $data = $encoding->encode( $data );
+ }
+ unshift @$writequeue, my $new = Writer(
+ $data, $head->writelen, $head->on_write, undef, undef, 0
+ );
+ next;
+ }
+ elsif( blessed $head->data and $head->data->isa( "Future" ) ) {
+ my $f = $head->data;
+ if( !$f->is_ready ) {
+ return 0 if $head->watching;
+ $f->on_ready( sub { $self->_flush_one_write } );
+ $head->watching++;
+ return 0;
+ }
+ my $data = $f->get;
+ if( !ref $data and my $encoding = $self->{encoding} ) {
+ $data = $encoding->encode( $data );
+ }
+ $head->data = $data;
+ next;
+ }
+ else {
+ die "Unsure what to do with reference ".ref($head->data)." in write queue";
+ }
+ }
+
+ my $second;
+ while( $second = $writequeue->[1] and
+ !ref $second->data and
+ $head->writelen == $second->writelen and
+ !$head->on_write and !$second->on_write and
+ !$head->on_flush ) {
+ $head->data .= $second->data;
+ $head->on_write = $second->on_write;
+ $head->on_flush = $second->on_flush;
+ splice @$writequeue, 1, 1, ();
+ }
+
+ die "TODO: head data does not contain a plain string" if ref $head->data;
+
+ if( $IO::Async::Debug::DEBUG > 1 ) {
+ my $data = substr $head->data, 0, $head->writelen;
+ $self->debug_printf( "WRITE len=%d", length $data );
+ IO::Async::Debug::log_hexdump( $data ) if $IO::Async::Debug::DEBUG_FLAGS{Sw};
+ }
+
+ my $writer = $self->{writer};
+ my $len = $self->$writer( $self->write_handle, $head->data, $head->writelen );
+
+ if( !defined $len ) {
+ my $errno = $!;
+
+ if( $errno == EAGAIN or $errno == EWOULDBLOCK ) {
+ $self->maybe_invoke_event( on_writeable_stop => ) if $self->{writeable};
+ $self->{writeable} = 0;
+ }
+
+ return 0 if _nonfatal_error( $errno );
+
+ if( $errno == EPIPE ) {
+ $self->{write_eof} = 1;
+ $self->maybe_invoke_event( on_write_eof => );
+ }
+
+ $head->on_error->( $self, $errno ) if $head->on_error;
+ $self->maybe_invoke_event( on_write_error => $errno )
+ or $self->close_now;
+
+ return 0;
+ }
+
+ if( my $on_write = $head->on_write ) {
+ $on_write->( $self, $len );
+ }
+
+ if( !length $head->data ) {
+ $head->on_flush->( $self ) if $head->on_flush;
+ shift @{ $self->{writequeue} };
+ }
+
+ return 1;
+}
+
+sub write
+{
+ my $self = shift;
+ my ( $data, %params ) = @_;
+
+ carp "Cannot write data to a Stream that is closing" and return if $self->{stream_closing};
+
+ # Allow writes without a filehandle if we're not yet in a Loop, just don't
+ # try to flush them
+ my $handle = $self->write_handle;
+
+ croak "Cannot write data to a Stream with no write_handle" if !$handle and $self->loop;
+
+ if( !ref $data and my $encoding = $self->{encoding} ) {
+ $data = $encoding->encode( $data );
+ }
+
+ my $on_write = delete $params{on_write};
+ my $on_flush = delete $params{on_flush};
+ my $on_error = delete $params{on_error};
+
+ my $f;
+ if( defined wantarray ) {
+ my $orig_on_flush = $on_flush;
+ my $orig_on_error = $on_error;
+
+ my $loop = $self->loop or
+ croak "Cannot ->write data returning a Future to a Stream not in a Loop";
+ $f = $loop->new_future;
+ $on_flush = sub {
+ $f->done;
+ $orig_on_flush->( @_ ) if $orig_on_flush;
+ };
+ $on_error = sub {
+ my $self = shift;
+ my ( $errno ) = @_;
+
+ $f->fail( "write failed: $errno", syswrite => $errno ) unless $f->is_ready;
+
+ $orig_on_error->( $self, @_ ) if $orig_on_error;
+ };
+ }
+
+ push @{ $self->{writequeue} }, Writer(
+ $data, $params{write_len} // $self->{write_len}, $on_write, $on_flush, $on_error, 0
+ );
+
+ keys %params and croak "Unrecognised keys for ->write - " . join( ", ", keys %params );
+
+ return $f unless $handle;
+
+ if( $self->{autoflush} ) {
+ 1 while !$self->_is_empty and $self->_flush_one_write;
+
+ if( $self->_is_empty ) {
+ $self->want_writeready_for_write( 0 );
+ return $f;
+ }
+ }
+
+ $self->want_writeready_for_write( 1 );
+ return $f;
+}
+
+sub on_write_ready
+{
+ my $self = shift;
+
+ if( !$self->{writeable} ) {
+ $self->maybe_invoke_event( on_writeable_start => );
+ $self->{writeable} = 1;
+ }
+
+ $self->_do_write if $self->{want} & WANT_WRITE_FOR_WRITE;
+ $self->_do_read if $self->{want} & WANT_WRITE_FOR_READ;
+}
+
+sub _do_write
+{
+ my $self = shift;
+
+ 1 while !$self->_is_empty and $self->_flush_one_write and $self->{write_all};
+
+ # All data successfully flushed
+ if( $self->_is_empty ) {
+ $self->want_writeready_for_write( 0 );
+
+ $self->maybe_invoke_event( on_outgoing_empty => );
+
+ $self->close_now if $self->{stream_closing};
+ }
+}
+
+sub _flush_one_read
+{
+ my $self = shift;
+ my ( $eof ) = @_;
+
+ local $self->{flushing_read} = 1;
+
+ my $readqueue = $self->{readqueue};
+
+ my $ret;
+ if( $readqueue->[0] and my $on_read = $readqueue->[0]->on_read ) {
+ $ret = $on_read->( $self, \$self->{readbuff}, $eof );
+ }
+ else {
+ $ret = $self->invoke_event( on_read => \$self->{readbuff}, $eof );
+ }
+
+ if( defined $self->{read_low_watermark} and $self->{at_read_high_watermark} and
+ length $self->{readbuff} < $self->{read_low_watermark} ) {
+ undef $self->{at_read_high_watermark};
+ $self->invoke_event( on_read_low_watermark => length $self->{readbuff} );
+ }
+
+ if( ref $ret eq "CODE" ) {
+ # Replace the top CODE, or add it if there was none
+ $readqueue->[0] = Reader( $ret, undef );
+ return 1;
+ }
+ elsif( @$readqueue and !defined $ret ) {
+ shift @$readqueue;
+ return 1;
+ }
+ else {
+ return $ret && ( length( $self->{readbuff} ) > 0 || $eof );
+ }
+}
+
+sub _sysread
+{
+ my $self = shift;
+ my ( $handle, undef, $len ) = @_;
+ return $handle->sysread( $_[1], $len );
+}
+
+sub on_read_ready
+{
+ my $self = shift;
+
+ $self->_do_read if $self->{want} & WANT_READ_FOR_READ;
+ $self->_do_write if $self->{want} & WANT_READ_FOR_WRITE;
+}
+
+sub _do_read
+{
+ my $self = shift;
+
+ my $handle = $self->read_handle;
+ my $reader = $self->{reader};
+
+ while(1) {
+ my $data;
+ my $len = $self->$reader( $handle, $data, $self->{read_len} );
+
+ if( !defined $len ) {
+ my $errno = $!;
+
+ return if _nonfatal_error( $errno );
+
+ $self->maybe_invoke_event( on_read_error => $errno )
+ or $self->close_now;
+
+ foreach ( @{ $self->{readqueue} } ) {
+ $_->future->fail( "read failed: $errno", sysread => $errno ) if $_->future;
+ }
+ undef @{ $self->{readqueue} };
+
+ return;
+ }
+
+ if( $IO::Async::Debug::DEBUG > 1 ) {
+ $self->debug_printf( "READ len=%d", $len );
+ IO::Async::Debug::log_hexdump( $data ) if $IO::Async::Debug::DEBUG_FLAGS{Sr};
+ }
+
+ my $eof = $self->{read_eof} = ( $len == 0 );
+
+ if( my $encoding = $self->{encoding} ) {
+ my $bytes = defined $self->{bytes_remaining} ? $self->{bytes_remaining} . $data : $data;
+ $data = $encoding->decode( $bytes, STOP_AT_PARTIAL );
+ $self->{bytes_remaining} = $bytes;
+ }
+
+ $self->{readbuff} .= $data if !$eof;
+
+ 1 while $self->_flush_one_read( $eof );
+
+ if( $eof ) {
+ $self->maybe_invoke_event( on_read_eof => );
+ $self->close_now if $self->{close_on_read_eof};
+ foreach ( @{ $self->{readqueue} } ) {
+ $_->future->done( undef ) if $_->future;
+ }
+ undef @{ $self->{readqueue} };
+ return;
+ }
+
+ last unless $self->{read_all};
+ }
+
+ if( defined $self->{read_high_watermark} and length $self->{readbuff} >= $self->{read_high_watermark} ) {
+ $self->{at_read_high_watermark} or
+ $self->invoke_event( on_read_high_watermark => length $self->{readbuff} );
+
+ $self->{at_read_high_watermark} = 1;
+ }
+}
+
+sub on_read_high_watermark
+{
+ my $self = shift;
+ $self->want_readready_for_read( 0 );
+}
+
+sub on_read_low_watermark
+{
+ my $self = shift;
+ $self->want_readready_for_read( 1 );
+}
+
+=head2 $stream->push_on_read( $on_read )
+
+Pushes a new temporary C<on_read> handler to the end of the queue. This queue,
+if non-empty, is used to provide C<on_read> event handling code in preference
+to using the object's main event handler or method. New handlers can be
+supplied at any time, and they will be used in first-in first-out (FIFO)
+order.
+
+As with the main C<on_read> event handler, each can return a (defined) boolean
+to indicate if they wish to be invoked again or not, another C<CODE> reference
+to replace themself with, or C<undef> to indicate it is now complete and
+should be removed. When a temporary handler returns C<undef> it is shifted
+from the queue and the next one, if present, is invoked instead. If there are
+no more then the object's main handler is invoked instead.
+
+=cut
+
+sub push_on_read
+{
+ my $self = shift;
+ my ( $on_read, %args ) = @_;
+ # %args undocumented for internal use
+
+ push @{ $self->{readqueue} }, Reader( $on_read, $args{future} );
+
+ # TODO: Should this always defer?
+ return if $self->{flushing_read};
+ 1 while length $self->{readbuff} and $self->_flush_one_read( 0 );
+}
+
+=head1 FUTURE-RETURNING READ METHODS
+
+The following methods all return a L<Future> which will become ready when
+enough data has been read by the Stream into its buffer. At this point, the
+data is removed from the buffer and given to the C<Future> object to complete
+it.
+
+ my $f = $stream->read_...
+
+ my ( $string ) = $f->get;
+
+Unlike the C<on_read> event handlers, these methods don't allow for access to
+"partial" results; they only provide the final result once it is ready.
+
+If a C<Future> is cancelled before it completes it is removed from the read
+queue without consuming any data; i.e. each C<Future> atomically either
+completes or is cancelled.
+
+Since it is possible to use a readable C<Stream> entirely using these
+C<Future>-returning methods instead of the C<on_read> event, it may be useful
+to configure a trivial return-false event handler to keep it from consuming
+any input, and to allow it to be added to a C<Loop> in the first place.
+
+ my $stream = IO::Async::Stream->new( on_read => sub { 0 }, ... );
+ $loop->add( $stream );
+
+ my $f = $stream->read_...
+
+If a read EOF or error condition happens while there are read C<Future>s
+pending, they are all completed. In the case of a read EOF, they are done with
+C<undef>; in the case of a read error they are failed using the C<$!> error
+value as the failure.
+
+ $f->fail( $message, sysread => $! )
+
+If a read EOF condition happens to the currently-processing read C<Future>, it
+will return a partial result. The calling code can detect this by the fact
+that the returned data is not complete according to the specification (too
+short in C<read_exactly>'s case, or lacking the ending pattern in
+C<read_until>'s case). Additionally, each C<Future> will yield the C<$eof>
+value in its results.
+
+ my ( $string, $eof ) = $f->get;
+
+=cut
+
+sub _read_future
+{
+ my $self = shift;
+ my $f = $self->loop->new_future;
+ $f->on_cancel( $self->_capture_weakself( sub {
+ my $self = shift or return;
+ 1 while $self->_flush_one_read;
+ }));
+ return $f;
+}
+
+=head2 ( $string, $eof ) = $stream->read_atmost( $len )->get
+
+=head2 ( $string, $eof ) = $stream->read_exactly( $len )->get
+
+Completes the C<Future> when the read buffer contains C<$len> or more
+characters of input. C<read_atmost> will also complete after the first
+invocation of C<on_read>, even if fewer characters are available, whereas
+C<read_exactly> will wait until at least C<$len> are available.
+
+=cut
+
+sub read_atmost
+{
+ my $self = shift;
+ my ( $len ) = @_;
+
+ my $f = $self->_read_future;
+ $self->push_on_read( sub {
+ my ( undef, $buffref, $eof ) = @_;
+ return undef if $f->is_cancelled;
+ $f->done( substr( $$buffref, 0, $len, "" ), $eof );
+ return undef;
+ }, future => $f );
+ return $f;
+}
+
+sub read_exactly
+{
+ my $self = shift;
+ my ( $len ) = @_;
+
+ my $f = $self->_read_future;
+ $self->push_on_read( sub {
+ my ( undef, $buffref, $eof ) = @_;
+ return undef if $f->is_cancelled;
+ return 0 unless $eof or length $$buffref >= $len;
+ $f->done( substr( $$buffref, 0, $len, "" ), $eof );
+ return undef;
+ }, future => $f );
+ return $f;
+}
+
+=head2 ( $string, $eof ) = $stream->read_until( $end )->get
+
+Completes the C<Future> when the read buffer contains a match for C<$end>,
+which may either be a plain string or a compiled C<Regexp> reference. Yields
+the prefix of the buffer up to and including this match.
+
+=cut
+
+sub read_until
+{
+ my $self = shift;
+ my ( $until ) = @_;
+
+ ref $until or $until = qr/\Q$until\E/;
+
+ my $f = $self->_read_future;
+ $self->push_on_read( sub {
+ my ( undef, $buffref, $eof ) = @_;
+ return undef if $f->is_cancelled;
+ if( $$buffref =~ $until ) {
+ $f->done( substr( $$buffref, 0, $+[0], "" ), $eof );
+ return undef;
+ }
+ elsif( $eof ) {
+ $f->done( $$buffref, $eof ); $$buffref = "";
+ return undef;
+ }
+ else {
+ return 0;
+ }
+ }, future => $f );
+ return $f;
+}
+
+=head2 ( $string, $eof ) = $stream->read_until_eof->get
+
+Completes the C<Future> when the stream is eventually closed at EOF, and
+yields all of the data that was available.
+
+=cut
+
+sub read_until_eof
+{
+ my $self = shift;
+
+ my $f = $self->_read_future;
+ $self->push_on_read( sub {
+ my ( undef, $buffref, $eof ) = @_;
+ return undef if $f->is_cancelled;
+ return 0 unless $eof;
+ $f->done( $$buffref, $eof ); $$buffref = "";
+ return undef;
+ }, future => $f );
+ return $f;
+}
+
+=head1 UTILITY CONSTRUCTORS
+
+=cut
+
+=head2 $stream = IO::Async::Stream->new_for_stdin
+
+=head2 $stream = IO::Async::Stream->new_for_stdout
+
+=head2 $stream = IO::Async::Stream->new_for_stdio
+
+Return a C<IO::Async::Stream> object preconfigured with the correct
+C<read_handle>, C<write_handle> or both.
+
+=cut
+
+sub new_for_stdin { shift->new( read_handle => \*STDIN, @_ ) }
+sub new_for_stdout { shift->new( write_handle => \*STDOUT, @_ ) }
+
+sub new_for_stdio { shift->new( read_handle => \*STDIN, write_handle => \*STDOUT, @_ ) }
+
+=head2 $future = $stream->connect( %args )
+
+A convenient wrapper for calling the C<connect> method on the underlying
+L<IO::Async::Loop> object, passing the C<socktype> hint as C<stream> if not
+otherwise supplied.
+
+=cut
+
+sub connect
+{
+ my $self = shift;
+ return $self->SUPER::connect( socktype => "stream", @_ );
+}
+
+=head1 DEBUGGING FLAGS
+
+The following flags in C<IO_ASYNC_DEBUG_FLAGS> enable extra logging:
+
+=over 4
+
+=item C<Sr>
+
+Log byte buffers as data is read from a Stream
+
+=item C<Sw>
+
+Log byte buffers as data is written to a Stream
+
+=back
+
+=cut
+
+=head1 EXAMPLES
+
+=head2 A line-based C<on_read> method
+
+The following C<on_read> method accepts incoming C<\n>-terminated lines and
+prints them to the program's C<STDOUT> stream.
+
+ sub on_read
+ {
+ my $self = shift;
+ my ( $buffref, $eof ) = @_;
+
+ while( $$buffref =~ s/^(.*\n)// ) {
+ print "Received a line: $1";
+ }
+
+ return 0;
+ }
+
+Because a reference to the buffer itself is passed, it is simple to use a
+C<s///> regular expression on the scalar it points at, to both check if data
+is ready (i.e. a whole line), and to remove it from the buffer. If no data is
+available then C<0> is returned, to indicate it should not be tried again. If
+a line was successfully extracted, then C<1> is returned, to indicate it
+should try again in case more lines exist in the buffer.
+
+=head2 Reading binary data
+
+This C<on_read> method accepts incoming records in 16-byte chunks, printing
+each one.
+
+ sub on_read
+ {
+ my ( $self, $buffref, $eof ) = @_;
+
+ if( length $$buffref >= 16 ) {
+ my $record = substr( $$buffref, 0, 16, "" );
+ print "Received a 16-byte record: $record\n";
+
+ return 1;
+ }
+
+ if( $eof and length $$buffref ) {
+ print "EOF: a partial record still exists\n";
+ }
+
+ return 0;
+ }
+
+The 4-argument form of C<substr()> extracts the 16-byte record from the buffer
+and assigns it to the C<$record> variable, if there was enough data in the
+buffer to extract it.
+
+A lot of protocols use a fixed-size header, followed by a variable-sized body
+of data, whose size is given by one of the fields of the header. The following
+C<on_read> method extracts messages in such a protocol.
+
+ sub on_read
+ {
+ my ( $self, $buffref, $eof ) = @_;
+
+ return 0 unless length $$buffref >= 8; # "N n n" consumes 8 bytes
+
+ my ( $len, $x, $y ) = unpack "N n n", $$buffref;
+
+ return 0 unless length $$buffref >= 8 + $len;
+
+ substr( $$buffref, 0, 8, "" );
+ my $data = substr( $$buffref, 0, $len, "" );
+
+ print "A record with values x=$x y=$y\n";
+
+ return 1;
+ }
+
+In this example, the header is C<unpack()>ed first, to extract the body
+length, and then the body is extracted. If the buffer does not have enough
+data yet for a complete message then C<0> is returned, and the buffer is left
+unmodified for next time. Only when there are enough bytes in total does it
+use C<substr()> to remove them.
+
+=head2 Dynamic replacement of C<on_read>
+
+Consider the following protocol (inspired by IMAP), which consists of
+C<\n>-terminated lines that may have an optional data block attached. The
+presence of such a data block, as well as its size, is indicated by the line
+prefix.
+
+ sub on_read
+ {
+ my $self = shift;
+ my ( $buffref, $eof ) = @_;
+
+ if( $$buffref =~ s/^DATA (\d+):(.*)\n// ) {
+ my $length = $1;
+ my $line = $2;
+
+ return sub {
+ my $self = shift;
+ my ( $buffref, $eof ) = @_;
+
+ return 0 unless length $$buffref >= $length;
+
+ # Take and remove the data from the buffer
+ my $data = substr( $$buffref, 0, $length, "" );
+
+ print "Received a line $line with some data ($data)\n";
+
+ return undef; # Restore the original method
+ }
+ }
+ elsif( $$buffref =~ s/^LINE:(.*)\n// ) {
+ my $line = $1;
+
+ print "Received a line $line with no data\n";
+
+ return 1;
+ }
+ else {
+ print STDERR "Unrecognised input\n";
+ # Handle it somehow
+ }
+ }
+
+In the case where trailing data is supplied, a new temporary C<on_read>
+callback is provided in a closure. This closure captures the C<$length>
+variable so it knows how much data to expect. It also captures the C<$line>
+variable so it can use it in the event report. When this method has finished
+reading the data, it reports the event, then restores the original method by
+returning C<undef>.
+
+=head1 SEE ALSO
+
+=over 4
+
+=item *
+
+L<IO::Handle> - Supply object methods for I/O handles
+
+=back
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/Test.pm b/lib/IO/Async/Test.pm
new file mode 100644
index 0000000..933330f
--- /dev/null
+++ b/lib/IO/Async/Test.pm
@@ -0,0 +1,185 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2007-2012 -- leonerd@leonerd.org.uk
+
+package IO::Async::Test;
+
+use strict;
+use warnings;
+
+our $VERSION = '0.67';
+
+use Exporter 'import';
+our @EXPORT = qw(
+ testing_loop
+ wait_for
+ wait_for_stream
+);
+
+=head1 NAME
+
+C<IO::Async::Test> - utility functions for use in test scripts
+
+=head1 SYNOPSIS
+
+ use Test::More tests => 1;
+ use IO::Async::Test;
+
+ use IO::Async::Loop;
+ my $loop = IO::Async::Loop->new;
+ testing_loop( $loop );
+
+ my $result;
+
+ $loop->do_something(
+ some => args,
+
+ on_done => sub {
+ $result = the_outcome;
+ }
+ );
+
+ wait_for { defined $result };
+
+ is( $result, what_we_expected, 'The event happened' );
+
+ ...
+
+ my $buffer = "";
+ my $handle = IO::Handle-> ...
+
+ wait_for_stream { length $buffer >= 10 } $handle => $buffer;
+
+ is( substr( $buffer, 0, 10, "" ), "0123456789", 'Buffer was correct' );
+
+=head1 DESCRIPTION
+
+This module provides utility functions that may be useful when writing test
+scripts for code which uses C<IO::Async> (as well as being used in the
+C<IO::Async> test scripts themselves).
+
+Test scripts are often synchronous by nature; they are a linear sequence of
+actions to perform, interspersed with assertions which check for given
+conditions. This goes against the very nature of C<IO::Async> which, being an
+asynchronisation framework, does not provide a linear stepped way of working.
+
+In order to write a test, the C<wait_for> function provides a way of
+synchronising the code, so that a given condition is known to hold, which
+would typically signify that some event has occured, the outcome of which can
+now be tested using the usual testing primitives.
+
+Because the primary purpose of C<IO::Async> is to provide IO operations on
+filehandles, a great many tests will likely be based around connected pipes or
+socket handles. The C<wait_for_stream> function provides a convenient way
+to wait for some content to be written through such a connected stream.
+
+=cut
+
+my $loop;
+END { undef $loop }
+
+=head1 FUNCTIONS
+
+=cut
+
+=head2 testing_loop( $loop )
+
+Set the C<IO::Async::Loop> object which the C<wait_for> function will loop
+on.
+
+=cut
+
+sub testing_loop
+{
+ $loop = shift;
+}
+
+=head2 wait_for( $condfunc )
+
+Repeatedly call the C<loop_once> method on the underlying loop (given to the
+C<testing_loop> function), until the given condition function callback
+returns true.
+
+To guard against stalled scripts, if the loop indicates a timeout for 10
+consequentive seconds, then an error is thrown.
+
+=cut
+
+sub wait_for(&)
+{
+ my ( $cond ) = @_;
+
+ my ( undef, $callerfile, $callerline ) = caller;
+
+ my $timedout = 0;
+ my $timerid = $loop->watch_time(
+ after => 10,
+ code => sub { $timedout = 1 },
+ );
+
+ $loop->loop_once( 1 ) while !$cond->() and !$timedout;
+
+ if( $timedout ) {
+ die "Nothing was ready after 10 second wait; called at $callerfile line $callerline\n";
+ }
+ else {
+ $loop->unwatch_time( $timerid );
+ }
+}
+
+=head2 wait_for_stream( $condfunc, $handle, $buffer )
+
+As C<wait_for>, but will also watch the given IO handle for readability, and
+whenever it is readable will read bytes in from it into the given buffer. The
+buffer is NOT initialised when the function is entered, in case data remains
+from a previous call.
+
+C<$buffer> can also be a CODE reference, in which case it will be invoked
+being passed data read from the handle, whenever it is readable.
+
+=cut
+
+sub wait_for_stream(&$$)
+{
+ my ( $cond, $handle, undef ) = @_;
+
+ my $on_read;
+ if( ref $_[2] eq "CODE" ) {
+ $on_read = $_[2];
+ }
+ else {
+ my $varref = \$_[2];
+ $on_read = sub { $$varref .= $_[0] };
+ }
+
+ $loop->watch_io(
+ handle => $handle,
+ on_read_ready => sub {
+ my $ret = $handle->sysread( my $buffer, 8192 );
+ if( !defined $ret ) {
+ die "Read failed on $handle - $!\n";
+ }
+ elsif( $ret == 0 ) {
+ die "Read returned EOF on $handle\n";
+ }
+ $on_read->( $buffer );
+ }
+ );
+
+ # Have to defeat the prototype... grr I hate these
+ &wait_for( $cond );
+
+ $loop->unwatch_io(
+ handle => $handle,
+ on_read_ready => 1,
+ );
+}
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/Timer.pm b/lib/IO/Async/Timer.pm
new file mode 100644
index 0000000..8e5961b
--- /dev/null
+++ b/lib/IO/Async/Timer.pm
@@ -0,0 +1,187 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2009-2012 -- leonerd@leonerd.org.uk
+
+package IO::Async::Timer;
+
+use strict;
+use warnings;
+use base qw( IO::Async::Notifier );
+
+our $VERSION = '0.67';
+
+use Carp;
+
+=head1 NAME
+
+C<IO::Async::Timer> - base class for Notifiers that use timed delays
+
+=head1 DESCRIPTION
+
+This module provides a subclass of L<IO::Async::Notifier> for implementing
+notifiers that use timed delays. For specific implementations, see one of the
+subclasses:
+
+=over 8
+
+=item *
+
+L<IO::Async::Timer::Absolute> - event callback at a fixed future time
+
+=item *
+
+L<IO::Async::Timer::Countdown> - event callback after a fixed delay
+
+=item *
+
+L<IO::Async::Timer::Periodic> - event callback at regular intervals
+
+=back
+
+=cut
+
+=head1 CONSTRUCTOR
+
+=cut
+
+=head2 $timer = IO::Async::Timer->new( %args )
+
+Constructs a particular subclass of C<IO::Async::Timer> object, and returns
+it. This constructor is provided for backward compatibility to older code
+which doesn't use the subclasses. New code should directly construct a
+subclass instead.
+
+=over 8
+
+=item mode => STRING
+
+The type of timer to create. Currently the only allowed mode is C<countdown>
+but more types may be added in the future.
+
+=back
+
+Once constructed, the C<Timer> will need to be added to the C<Loop> before it
+will work. It will also need to be started by the C<start> method.
+
+=cut
+
+sub new
+{
+ my $class = shift;
+ my %args = @_;
+
+ if( my $mode = delete $args{mode} ) {
+ # Might define some other modes later
+ $mode eq "countdown" or croak "Expected 'mode' to be 'countdown'";
+
+ require IO::Async::Timer::Countdown;
+ return IO::Async::Timer::Countdown->new( %args );
+ }
+
+ return $class->SUPER::new( %args );
+}
+
+sub _add_to_loop
+{
+ my $self = shift;
+ $self->start if delete $self->{pending};
+}
+
+sub _remove_from_loop
+{
+ my $self = shift;
+ $self->stop;
+}
+
+=head1 METHODS
+
+=cut
+
+=head2 $running = $timer->is_running
+
+Returns true if the Timer has been started, and has not yet expired, or been
+stopped.
+
+=cut
+
+sub is_running
+{
+ my $self = shift;
+
+ defined $self->{id};
+}
+
+=head2 $timer->start
+
+Starts the Timer. Throws an error if it was already running.
+
+If the Timer is not yet in a Loop, the actual start will be deferred until it
+is added. Once added, it will be running, and will expire at the given
+duration after the time it was added.
+
+As a convenience, C<$timer> is returned. This may be useful for starting
+timers at construction time:
+
+ $loop->add( IO::Async::Timer->new( ... )->start );
+
+=cut
+
+sub start
+{
+ my $self = shift;
+
+ my $loop = $self->loop;
+ if( !defined $loop ) {
+ $self->{pending} = 1;
+ return $self;
+ }
+
+ defined $self->{id} and croak "Cannot start a Timer that is already running";
+
+ if( !$self->{cb} ) {
+ $self->{cb} = $self->_make_cb;
+ }
+
+ $self->{id} = $loop->watch_time(
+ $self->_make_enqueueargs,
+ code => $self->{cb},
+ );
+
+ return $self;
+}
+
+=head2 $timer->stop
+
+Stops the Timer if it is running. If it has not yet been added to the C<Loop>
+but there is a start pending, this will cancel it.
+
+=cut
+
+sub stop
+{
+ my $self = shift;
+
+ if( $self->{pending} ) {
+ delete $self->{pending};
+ return;
+ }
+
+ return if !$self->is_running;
+
+ my $loop = $self->loop or croak "Cannot stop a Timer that is not in a Loop";
+
+ defined $self->{id} or return; # nothing to do but no error
+
+ $loop->unwatch_time( $self->{id} );
+
+ undef $self->{id};
+}
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/Timer/Absolute.pm b/lib/IO/Async/Timer/Absolute.pm
new file mode 100644
index 0000000..a925415
--- /dev/null
+++ b/lib/IO/Async/Timer/Absolute.pm
@@ -0,0 +1,142 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2010-2015 -- leonerd@leonerd.org.uk
+
+package IO::Async::Timer::Absolute;
+
+use strict;
+use warnings;
+use base qw( IO::Async::Timer );
+
+our $VERSION = '0.67';
+
+use Carp;
+
+=head1 NAME
+
+C<IO::Async::Timer::Absolute> - event callback at a fixed future time
+
+=head1 SYNOPSIS
+
+ use IO::Async::Timer::Absolute;
+
+ use POSIX qw( mktime );
+
+ use IO::Async::Loop;
+ my $loop = IO::Async::Loop->new;
+
+ my @time = gmtime;
+
+ my $timer = IO::Async::Timer::Absolute->new(
+ time => mktime( 0, 0, 0, $time[3]+1, $time[4], $time[5] ),
+
+ on_expire => sub {
+ print "It's midnight\n";
+ $loop->stop;
+ },
+ );
+
+ $loop->add( $timer );
+
+ $loop->run;
+
+=head1 DESCRIPTION
+
+This subclass of L<IO::Async::Timer> implements one-shot events at a fixed
+time in the future. The object waits for a given timestamp, and invokes its
+callback at that point in the future.
+
+For a C<Timer> object that waits for a delay relative to the time it is
+started, see instead L<IO::Async::Timer::Countdown>.
+
+=cut
+
+=head1 EVENTS
+
+The following events are invoked, either using subclass methods or CODE
+references in parameters:
+
+=head2 on_expire
+
+Invoked when the timer expires.
+
+=cut
+
+=head1 PARAMETERS
+
+The following named parameters may be passed to C<new> or C<configure>:
+
+=head2 on_expire => CODE
+
+CODE reference for the C<on_expire> event.
+
+=head2 time => NUM
+
+The epoch time at which the timer will expire.
+
+Once constructed, the timer object will need to be added to the C<Loop> before
+it will work.
+
+Unlike other timers, it does not make sense to C<start> this object, because
+its expiry time is absolute, and not relative to the time it is started.
+
+=cut
+
+sub configure
+{
+ my $self = shift;
+ my %params = @_;
+
+ if( exists $params{on_expire} ) {
+ my $on_expire = delete $params{on_expire};
+ ref $on_expire or croak "Expected 'on_expire' as a reference";
+
+ $self->{on_expire} = $on_expire;
+ undef $self->{cb}; # Will be lazily constructed when needed
+ }
+
+ if( exists $params{time} ) {
+ my $time = delete $params{time};
+
+ $self->stop if $self->is_running;
+
+ $self->{time} = $time;
+
+ $self->start if !$self->is_running;
+ }
+
+ unless( $self->can_event( 'on_expire' ) ) {
+ croak 'Expected either a on_expire callback or an ->on_expire method';
+ }
+
+ $self->SUPER::configure( %params );
+}
+
+sub _make_cb
+{
+ my $self = shift;
+
+ return $self->_capture_weakself( sub {
+ my $self = shift or return;
+
+ undef $self->{id};
+
+ $self->invoke_event( "on_expire" );
+ } );
+}
+
+sub _make_enqueueargs
+{
+ my $self = shift;
+
+ return at => $self->{time};
+}
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/Timer/Countdown.pm b/lib/IO/Async/Timer/Countdown.pm
new file mode 100644
index 0000000..201ba42
--- /dev/null
+++ b/lib/IO/Async/Timer/Countdown.pm
@@ -0,0 +1,274 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2009-2012 -- leonerd@leonerd.org.uk
+
+package IO::Async::Timer::Countdown;
+
+use strict;
+use warnings;
+use base qw( IO::Async::Timer );
+
+our $VERSION = '0.67';
+
+use Carp;
+
+=head1 NAME
+
+C<IO::Async::Timer::Countdown> - event callback after a fixed delay
+
+=head1 SYNOPSIS
+
+ use IO::Async::Timer::Countdown;
+
+ use IO::Async::Loop;
+ my $loop = IO::Async::Loop->new;
+
+ my $timer = IO::Async::Timer::Countdown->new(
+ delay => 10,
+
+ on_expire => sub {
+ print "Sorry, your time's up\n";
+ $loop->stop;
+ },
+ );
+
+ $timer->start;
+
+ $loop->add( $timer );
+
+ $loop->run;
+
+=head1 DESCRIPTION
+
+This subclass of L<IO::Async::Timer> implements one-shot fixed delays.
+The object implements a countdown timer, which invokes its callback after the
+given period from when it was started. After it has expired the Timer may be
+started again, when it will wait the same period then invoke the callback
+again. A timer that is currently running may be stopped or reset.
+
+For a C<Timer> object that repeatedly runs a callback at regular intervals,
+see instead L<IO::Async::Timer::Periodic>. For a C<Timer> that invokes its
+callback at a fixed time in the future, see L<IO::Async::Timer::Absolute>.
+
+=cut
+
+=head1 EVENTS
+
+The following events are invoked, either using subclass methods or CODE
+references in parameters:
+
+=head2 on_expire
+
+Invoked when the timer expires.
+
+=cut
+
+=head1 PARAMETERS
+
+The following named parameters may be passed to C<new> or C<configure>:
+
+=head2 on_expire => CODE
+
+CODE reference for the C<on_expire> event.
+
+=head2 delay => NUM
+
+The delay in seconds after starting the timer until it expires. Cannot be
+changed if the timer is running. A timer with a zero delay expires
+"immediately".
+
+=head2 remove_on_expire => BOOL
+
+Optional. If true, remove this timer object from its parent notifier or
+containing loop when it expires. Defaults to false.
+
+Once constructed, the timer object will need to be added to the C<Loop> before
+it will work. It will also need to be started by the C<start> method.
+
+=cut
+
+sub configure
+{
+ my $self = shift;
+ my %params = @_;
+
+ foreach (qw( remove_on_expire )) {
+ $self->{$_} = delete $params{$_} if exists $params{$_};
+ }
+
+ if( exists $params{on_expire} ) {
+ my $on_expire = delete $params{on_expire};
+ ref $on_expire or croak "Expected 'on_expire' as a reference";
+
+ $self->{on_expire} = $on_expire;
+ undef $self->{cb}; # Will be lazily constructed when needed
+ }
+
+ if( exists $params{delay} ) {
+ $self->is_running and croak "Cannot configure 'delay' of a running timer\n";
+
+ my $delay = delete $params{delay};
+ $delay >= 0 or croak "Expected a 'delay' as a non-negative number";
+
+ $self->{delay} = $delay;
+ }
+
+ unless( $self->can_event( 'on_expire' ) ) {
+ croak 'Expected either a on_expire callback or an ->on_expire method';
+ }
+
+ $self->SUPER::configure( %params );
+}
+
+=head1 METHODS
+
+=cut
+
+=head2 $expired = $timer->is_expired
+
+Returns true if the Timer has already expired.
+
+=cut
+
+sub is_expired
+{
+ my $self = shift;
+ return $self->{expired};
+}
+
+sub _make_cb
+{
+ my $self = shift;
+
+ return $self->_capture_weakself( sub {
+ my $self = shift or return;
+
+ undef $self->{id};
+ $self->{expired} = 1;
+
+ $self->remove_from_parent if $self->{remove_on_expire};
+
+ $self->invoke_event( "on_expire" );
+ } );
+}
+
+sub _make_enqueueargs
+{
+ my $self = shift;
+
+ undef $self->{expired};
+ return after => $self->{delay};
+}
+
+=head2 $timer->reset
+
+If the timer is running, restart the countdown period from now. If the timer
+is not running, this method has no effect.
+
+=cut
+
+sub reset
+{
+ my $self = shift;
+
+ my $loop = $self->loop or croak "Cannot reset a Timer that is not in a Loop";
+
+ return if !$self->is_running;
+
+ $self->stop;
+ $self->start;
+}
+
+=head1 EXAMPLES
+
+=head2 Watchdog Timer
+
+Because the C<reset> method restarts a running countdown timer back to its
+full period, it can be used to implement a watchdog timer. This is a timer
+which will not expire provided the method is called at least as often as it
+is configured. If the method fails to be called, the timer will eventually
+expire and run its callback.
+
+For example, to expire an accepted connection after 30 seconds of inactivity:
+
+ ...
+
+ on_accept => sub {
+ my ( $newclient ) = @_;
+
+ my $watchdog = IO::Async::Timer::Countdown->new(
+ delay => 30,
+
+ on_expire => sub {
+ my $self = shift;
+
+ my $stream = $self->parent;
+ $stream->close;
+ },
+ );
+
+ my $stream = IO::Async::Stream->new(
+ handle => $newclient,
+
+ on_read => sub {
+ my ( $self, $buffref, $eof ) = @_;
+ $watchdog->reset;
+
+ ...
+ },
+
+ on_closed => sub {
+ $watchdog->stop;
+ },
+ ) );
+
+ $stream->add_child( $watchdog );
+ $watchdog->start;
+
+ $loop->add( $watchdog );
+ }
+
+Rather than setting up a lexical variable to store the Stream so that the
+Timer's C<on_expire> closure can call C<close> on it, the parent/child
+relationship between the two Notifier objects is used. At the time the Timer
+C<on_expire> closure is invoked, it will have been added as a child notifier
+of the Stream; this means the Timer's C<parent> method will return the Stream
+Notifier. This enables it to call C<close> without needing to capture a
+lexical variable, which would create a cyclic reference.
+
+=head2 Fixed-Delay Repeating Timer
+
+The C<on_expire> event fires a fixed delay after the C<start> method has begun
+the countdown. The C<start> method can be invoked again at some point during
+the C<on_expire> handling code, to create a timer that invokes its code
+regularly a fixed delay after the previous invocation has finished. This
+creates an arrangement similar to an L<IO::Async::Timer::Periodic>, except
+that it will wait until the previous invocation has indicated it is finished,
+before starting the countdown for the next call.
+
+ my $timer = IO::Async::Timer::Countdown->new(
+ delay => 60,
+
+ on_expire => sub {
+ my $self = shift;
+
+ start_some_operation(
+ on_complete => sub { $self->start },
+ );
+ },
+ );
+
+ $timer->start;
+ $loop->add( $timer );
+
+This example invokes the C<start_some_operation> function 60 seconds after the
+previous iteration has indicated it has finished.
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/IO/Async/Timer/Periodic.pm b/lib/IO/Async/Timer/Periodic.pm
new file mode 100644
index 0000000..f99a43c
--- /dev/null
+++ b/lib/IO/Async/Timer/Periodic.pm
@@ -0,0 +1,249 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2009-2015 -- leonerd@leonerd.org.uk
+
+package IO::Async::Timer::Periodic;
+
+use strict;
+use warnings;
+use base qw( IO::Async::Timer );
+
+our $VERSION = '0.67';
+
+use Carp;
+
+=head1 NAME
+
+C<IO::Async::Timer::Periodic> - event callback at regular intervals
+
+=head1 SYNOPSIS
+
+ use IO::Async::Timer::Periodic;
+
+ use IO::Async::Loop;
+ my $loop = IO::Async::Loop->new;
+
+ my $timer = IO::Async::Timer::Periodic->new(
+ interval => 60,
+
+ on_tick => sub {
+ print "You've had a minute\n";
+ },
+ );
+
+ $timer->start;
+
+ $loop->add( $timer );
+
+ $loop->run;
+
+=head1 DESCRIPTION
+
+This subclass of L<IO::Async::Timer> implements repeating events at regular
+clock intervals. The timing may or may not be subject to how long it takes the
+callback to execute. Iterations may be rescheduled runs at fixed regular
+intervals beginning at the time the timer was started, or by a fixed delay
+after the previous code has finished executing.
+
+For a C<Timer> object that only runs a callback once, after a given delay, see
+instead L<IO::Async::Timer::Countdown>. A Countdown timer can also be used to
+create repeating events that fire at a fixed delay after the previous event
+has finished processing. See als the examples in
+C<IO::Async::Timer::Countdown>.
+
+=cut
+
+=head1 EVENTS
+
+The following events are invoked, either using subclass methods or CODE
+references in parameters:
+
+=head2 on_tick
+
+Invoked on each interval of the timer.
+
+=cut
+
+=head1 PARAMETERS
+
+The following named parameters may be passed to C<new> or C<configure>:
+
+=head2 on_tick => CODE
+
+CODE reference for the C<on_tick> event.
+
+=head2 interval => NUM
+
+The interval in seconds between invocations of the callback or method. Cannot
+be changed if the timer is running.
+
+=head2 first_interval => NUM
+
+Optional. If defined, the interval in seconds after calling the C<start>
+method before the first invocation of the callback or method. Thereafter, the
+regular C<interval> will be used. If not supplied, the first interval will be
+the same as the others.
+
+Even if this value is zero, the first invocation will be made asynchronously,
+by the containing C<Loop> object, and not synchronously by the C<start> method
+itself.
+
+=head2 reschedule => STRING
+
+Optional. Must be one of C<hard>, C<skip> or C<drift>. Defines the algorithm
+used to reschedule the next invocation.
+
+C<hard> schedules each iteration at the fixed interval from the previous
+iteration's schedule time, ensuring a regular repeating event.
+
+C<skip> schedules similarly to C<hard>, but skips over times that have already
+passed. This matters if the duration is particularly short and there's a
+possibility that times may be missed, or if the entire process is stopped and
+resumed by C<SIGSTOP> or similar.
+
+C<drift> schedules each iteration at the fixed interval from the time that the
+previous iteration's event handler returns. This allows it to slowly drift over
+time and become desynchronised with other events of the same interval or
+multiples/fractions of it.
+
+Once constructed, the timer object will need to be added to the C<Loop> before
+it will work. It will also need to be started by the C<start> method.
+
+=cut
+
+sub _init
+{
+ my $self = shift;
+ $self->SUPER::_init( @_ );
+
+ $self->{reschedule} = "hard";
+}
+
+sub configure
+{
+ my $self = shift;
+ my %params = @_;
+
+ if( exists $params{on_tick} ) {
+ my $on_tick = delete $params{on_tick};
+ ref $on_tick or croak "Expected 'on_tick' as a reference";
+
+ $self->{on_tick} = $on_tick;
+ undef $self->{cb}; # Will be lazily constructed when needed
+ }
+
+ if( exists $params{interval} ) {
+ $self->is_running and croak "Cannot configure 'interval' of a running timer\n";
+
+ my $interval = delete $params{interval};
+ $interval > 0 or croak "Expected a 'interval' as a positive number";
+
+ $self->{interval} = $interval;
+ }
+
+ if( exists $params{first_interval} ) {
+ $self->is_running and croak "Cannot configure 'first_interval' of a running timer\n";
+
+ my $first_interval = delete $params{first_interval};
+ $first_interval >= 0 or croak "Expected a 'first_interval' as a non-negative number";
+
+ $self->{first_interval} = $first_interval;
+ }
+
+ if( exists $params{reschedule} ) {
+ my $resched = delete $params{reschedule} || "hard";
+ grep { $_ eq $resched } qw( hard skip drift ) or
+ croak "Expected 'reschedule' to be one of hard, skip, drift";
+
+ $self->{reschedule} = $resched;
+ }
+
+ unless( $self->can_event( 'on_tick' ) ) {
+ croak 'Expected either a on_tick callback or an ->on_tick method';
+ }
+
+ $self->SUPER::configure( %params );
+}
+
+sub _next_interval
+{
+ my $self = shift;
+ return $self->{first_interval} if defined $self->{first_interval};
+ return $self->{interval};
+}
+
+sub start
+{
+ my $self = shift;
+
+ # Only actually define a time if we've got a loop; otherwise it'll just
+ # become start-pending. We'll calculate it properly when it gets added to
+ # the Loop
+ if( my $loop = $self->loop ) {
+ my $now = $loop->time;
+ my $resched = $self->{reschedule};
+
+ if( !defined $self->{next_time} ) {
+ $self->{next_time} = $now + $self->_next_interval;
+ }
+ elsif( $resched eq "hard" ) {
+ $self->{next_time} += $self->_next_interval;
+ }
+ elsif( $resched eq "skip" ) {
+ # How many ticks are needed?
+ my $ticks = POSIX::ceil( $now - $self->{next_time} );
+ # $self->{last_ticks} = $ticks;
+ $self->{next_time} += $self->_next_interval * $ticks;
+ }
+ elsif( $resched eq "drift" ) {
+ $self->{next_time} = $now + $self->_next_interval;
+ }
+ }
+
+ $self->SUPER::start;
+}
+
+sub stop
+{
+ my $self = shift;
+ $self->SUPER::stop;
+
+ undef $self->{next_time};
+}
+
+sub _make_cb
+{
+ my $self = shift;
+
+ return $self->_capture_weakself( sub {
+ my $self = shift or return;
+
+ undef $self->{first_interval};
+
+ undef $self->{id};
+
+ my $ok = eval { $self->invoke_event( on_tick => ); 1 } or
+ my $e = $@;
+
+ # detect ->stop
+ $self->start if defined $self->{next_time};
+
+ die $e if !$ok;
+ } );
+}
+
+sub _make_enqueueargs
+{
+ my $self = shift;
+
+ return at => $self->{next_time};
+}
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/t/00use.t b/t/00use.t
new file mode 100644
index 0000000..268ef56
--- /dev/null
+++ b/t/00use.t
@@ -0,0 +1,35 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+
+use_ok( "IO::Async::Notifier" );
+use_ok( "IO::Async::Handle" );
+use_ok( "IO::Async::Stream" );
+use_ok( "IO::Async::Timer" );
+use_ok( "IO::Async::Timer::Absolute" );
+use_ok( "IO::Async::Timer::Countdown" );
+use_ok( "IO::Async::Timer::Periodic" );
+use_ok( "IO::Async::Signal" );
+use_ok( "IO::Async::Listener" );
+use_ok( "IO::Async::Socket" );
+use_ok( "IO::Async::File" );
+use_ok( "IO::Async::FileStream" );
+
+use_ok( "IO::Async::OS" );
+
+use_ok( "IO::Async::Loop::Select" );
+use_ok( "IO::Async::Loop::Poll" );
+
+use_ok( "IO::Async::Test" );
+
+use_ok( "IO::Async::Function" );
+use_ok( "IO::Async::Resolver" );
+
+use_ok( "IO::Async::Protocol" );
+use_ok( "IO::Async::Protocol::Stream" );
+use_ok( "IO::Async::Protocol::LineStream" );
+
+done_testing;
diff --git a/t/01timequeue.t b/t/01timequeue.t
new file mode 100644
index 0000000..0b05f10
--- /dev/null
+++ b/t/01timequeue.t
@@ -0,0 +1,94 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+use Test::Fatal;
+
+use IO::Async::Internals::TimeQueue;
+
+my $queue = IO::Async::Internals::TimeQueue->new;
+
+ok( defined $queue, '$queue defined' );
+isa_ok( $queue, "IO::Async::Internals::TimeQueue", '$queue isa IO::Async::Internals::TimeQueue' );
+
+is( $queue->next_time, undef, '->next_time when empty is undef' );
+
+ok( exception { $queue->enqueue( code => sub { "DUMMY" } ) },
+ 'enqueue no time fails' );
+
+ok( exception { $queue->enqueue( time => 123 ) },
+ 'enqueue no code fails' );
+
+ok( exception { $queue->enqueue( time => 123, code => 'HELLO' ) },
+ 'enqueue code not CODE ref fails' );
+
+$queue->enqueue( time => 1000, code => sub { "DUMMY" } );
+is( $queue->next_time, 1000, '->next_time after single enqueue' );
+
+my $fired = 0;
+
+$queue->enqueue( time => 500, code => sub { $fired = 1; } );
+is( $queue->next_time, 500, '->next_time after second enqueue' );
+
+my $count = $queue->fire( now => 700 );
+
+is( $fired, 1, '$fired after fire at time 700' );
+is( $count, 1, '$count after fire at time 700' );
+is( $queue->next_time, 1000, '->next_time after fire at time 700' );
+
+$count = $queue->fire( now => 900 );
+
+is( $count, 0, '$count after fire at time 900' );
+is( $queue->next_time, 1000, '->next_time after fire at time 900' );
+
+$count = $queue->fire( now => 1200 );
+
+is( $count, 1, '$count after fire at time 1200' );
+is( $queue->next_time, undef, '->next_time after fire at time 1200' );
+
+$queue->enqueue( time => 1300, code => sub{ $fired++; } );
+$queue->enqueue( time => 1301, code => sub{ $fired++; } );
+
+$count = $queue->fire( now => 1400 );
+
+is( $fired, 3, '$fired after fire at time 1400' );
+is( $count, 2, '$count after fire at time 1400' );
+is( $queue->next_time, undef, '->next_time after fire at time 1400' );
+
+my $id = $queue->enqueue( time => 1500, code => sub { $fired++ } );
+$queue->enqueue( time => 1505, code => sub { $fired++ } );
+
+is( $queue->next_time, 1500, '->next_time before cancel' );
+
+$queue->cancel( $id );
+
+is( $queue->next_time, 1505, '->next_time after cancel' );
+
+$fired = 0;
+$count = $queue->fire( now => 1501 );
+
+is( $fired, 0, '$fired after fire at time 1501' );
+is( $count, 0, '$count after fire at time 1501' );
+
+$count = $queue->fire( now => 1510 );
+
+is( $fired, 1, '$fired after fire at time 1510' );
+is( $count, 1, '$count after fire at time 1510' );
+
+# Performance for large collections
+{
+ foreach my $t ( 2000 .. 2100 ) {
+ $queue->enqueue( time => $t, code => sub {} );
+ }
+
+ foreach my $t ( 2000 .. 2100 ) {
+ $queue->next_time == $t or fail( "Failed for large collection - expected $t" ), last;
+ $queue->fire( now => $t );
+ }
+
+ ok( "Large collection" );
+}
+
+done_testing;
diff --git a/t/02os.t b/t/02os.t
new file mode 100644
index 0000000..b2d5a94
--- /dev/null
+++ b/t/02os.t
@@ -0,0 +1,170 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+use Test::Fatal;
+
+use IO::Async::OS;
+
+use Socket qw(
+ AF_INET AF_INET6 AF_UNIX SOCK_STREAM SOCK_DGRAM SO_TYPE
+ pack_sockaddr_in pack_sockaddr_in6 pack_sockaddr_un inet_aton inet_pton
+ INADDR_ANY
+);
+
+use POSIX qw( SIGTERM );
+
+SKIP: {
+ skip "No IO::Socket::IP", 2 unless eval { require IO::Socket::IP };
+
+ my $S_inet = IO::Async::OS->socket( "inet", "stream" );
+ isa_ok( $S_inet, "IO::Socket::IP", 'IO::Async::OS->socket("inet")' );
+
+ SKIP: {
+ skip "No AF_INET6", 1 unless eval { socket( my $fh, AF_INET6, SOCK_STREAM, 0 ) };
+
+ my $S_inet6 = IO::Async::OS->socket( "inet6", "stream" );
+ isa_ok( $S_inet6, "IO::Socket::IP", 'IO::Async::OS->socket("inet6")' );
+ }
+}
+
+foreach my $family ( undef, "inet" ) {
+ my ( $S1, $S2 ) = IO::Async::OS->socketpair( $family, "stream" )
+ or die "Could not socketpair - $!";
+
+ isa_ok( $S1, "IO::Socket", '$S1 isa IO::Socket' );
+ isa_ok( $S2, "IO::Socket", '$S2 isa IO::Socket' );
+
+ # Due to a bug in IO::Socket, ->socktype may not be set
+
+ is( $S1->sockopt(SO_TYPE), SOCK_STREAM, 'SO_TYPE of $S1 is SOCK_STREAM' );
+ is( $S2->sockopt(SO_TYPE), SOCK_STREAM, 'SO_TYPE of $S2 is SOCK_STREAM' );
+
+ $S1->syswrite( "Hello" );
+ is( do { my $b; $S2->sysread( $b, 8192 ); $b }, "Hello", '$S1 --writes-> $S2' );
+
+ $S2->syswrite( "Goodbye" );
+ is( do { my $b; $S1->sysread( $b, 8192 ); $b }, "Goodbye", '$S2 --writes-> $S1' );
+
+ ( $S1, $S2 ) = IO::Async::OS->socketpair( $family, "dgram" )
+ or die "Could not socketpair - $!";
+
+ isa_ok( $S1, "IO::Socket", '$S1 isa IO::Socket' );
+ isa_ok( $S2, "IO::Socket", '$S2 isa IO::Socket' );
+
+ is( $S1->socktype, SOCK_DGRAM, '$S1->socktype is SOCK_DGRAM' );
+ is( $S2->socktype, SOCK_DGRAM, '$S2->socktype is SOCK_DGRAM' );
+
+ $S1->syswrite( "Hello" );
+ is( do { my $b; $S2->sysread( $b, 8192 ); $b }, "Hello", '$S1 --writes-> $S2' );
+
+ $S2->syswrite( "Goodbye" );
+ is( do { my $b; $S1->sysread( $b, 8192 ); $b }, "Goodbye", '$S2 --writes-> $S1' );
+}
+
+{
+ my ( $Prd, $Pwr ) = IO::Async::OS->pipepair or die "Could not pipepair - $!";
+
+ $Pwr->syswrite( "Hello" );
+ is( do { my $b; $Prd->sysread( $b, 8192 ); $b }, "Hello", '$Pwr --writes-> $Prd' );
+
+ # Writing to $Prd _may_ fail, but some systems might implement this as a
+ # socketpair instead. We won't test it just in case
+}
+
+{
+ my ( $rdA, $wrA, $rdB, $wrB ) = IO::Async::OS->pipequad or die "Could not pipequad - $!";
+
+ $wrA->syswrite( "Hello" );
+ is( do { my $b; $rdA->sysread( $b, 8192 ); $b }, "Hello", '$wrA --writes-> $rdA' );
+
+ $wrB->syswrite( "Goodbye" );
+ is( do { my $b; $rdB->sysread( $b, 8192 ); $b }, "Goodbye", '$wrB --writes-> $rdB' );
+}
+
+is( IO::Async::OS->signame2num( 'TERM' ), SIGTERM, 'signame2num' );
+
+is( IO::Async::OS->getfamilybyname( "inet" ), AF_INET, 'getfamilybyname "inet"' );
+is( IO::Async::OS->getfamilybyname( AF_INET ), AF_INET, 'getfamilybyname AF_INET' );
+
+is( IO::Async::OS->getsocktypebyname( "stream" ), SOCK_STREAM, 'getsocktypebyname "stream"' );
+is( IO::Async::OS->getsocktypebyname( SOCK_STREAM ), SOCK_STREAM, 'getsocktypebyname SOCK_STREAM' );
+
+{
+ my $sinaddr = pack_sockaddr_in( 56, inet_aton( "1.2.3.4" ) );
+
+ is_deeply( [ IO::Async::OS->extract_addrinfo( [ "inet", "stream", 0, $sinaddr ] ) ],
+ [ AF_INET, SOCK_STREAM, 0, $sinaddr ],
+ 'extract_addrinfo( ARRAY )' );
+
+ is_deeply( [ IO::Async::OS->extract_addrinfo( {
+ family => "inet",
+ socktype => "stream",
+ addr => $sinaddr
+ } ) ],
+ [ AF_INET, SOCK_STREAM, 0, $sinaddr ],
+ 'extract_addrinfo( HASH )' );
+
+ is_deeply( [ IO::Async::OS->extract_addrinfo( {
+ family => "inet",
+ socktype => "stream",
+ ip => "1.2.3.4",
+ port => "56",
+ } ) ],
+ [ AF_INET, SOCK_STREAM, 0, $sinaddr ],
+ 'extract_addrinfo( HASH ) with inet, ip+port' );
+
+ is_deeply( [ IO::Async::OS->extract_addrinfo( {
+ family => "inet",
+ socktype => "stream",
+ port => "56",
+ } ) ],
+ [ AF_INET, SOCK_STREAM, 0, pack_sockaddr_in( 56, INADDR_ANY ) ],
+ 'extract_addrinfo( HASH ) with inet, port' );
+
+ is_deeply( [ IO::Async::OS->extract_addrinfo( {
+ family => "inet",
+ socktype => "stream",
+ } ) ],
+ [ AF_INET, SOCK_STREAM, 0, pack_sockaddr_in( 0, INADDR_ANY ) ],
+ 'extract_addrinfo( HASH ) with inet only' );
+
+ ok( exception { IO::Async::OS->extract_addrinfo( {
+ family => "inet",
+ host => "foobar.com",
+ } ) }, 'extract_addrinfo for inet complains about unrecognised key' );
+}
+
+SKIP: {
+ my $sin6addr = eval { Socket::pack_sockaddr_in6( 1234, inet_pton( AF_INET6, "fe80::5678" ) ) };
+ skip "No pack_sockaddr_in6", 1 unless defined $sin6addr;
+
+ is_deeply( [ IO::Async::OS->extract_addrinfo( {
+ family => "inet6",
+ socktype => "stream",
+ ip => "fe80::5678",
+ port => "1234",
+ } ) ],
+ [ AF_INET6, SOCK_STREAM, 0, $sin6addr ],
+ 'extract_addrinfo( HASH ) with inet6, ip+port' );
+}
+
+SKIP: {
+ skip "No pack_sockaddr_un", 1 unless IO::Async::OS->HAVE_SOCKADDR_UN;
+ my $sunaddr = pack_sockaddr_un( "foo.sock" );
+
+ is_deeply( [ IO::Async::OS->extract_addrinfo( {
+ family => "unix",
+ socktype => "stream",
+ path => "foo.sock",
+ } ) ],
+ [ AF_UNIX, SOCK_STREAM, 0, $sunaddr ],
+ 'extract_addrinfo( HASH ) with unix, path' );
+}
+
+ok( exception { IO::Async::OS->extract_addrinfo( { family => "hohum" } ) },
+ 'extract_addrinfo on unrecognised family complains' );
+
+done_testing;
diff --git a/t/03loop-magic.t b/t/03loop-magic.t
new file mode 100644
index 0000000..ede4105
--- /dev/null
+++ b/t/03loop-magic.t
@@ -0,0 +1,51 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+
+use IO::Async::Loop;
+
+$IO::Async::Loop::LOOP_NO_OS = 1;
+delete $ENV{IO_ASYNC_LOOP}; # Just in case it was already set
+
+my $loop;
+
+my $LOOPCLASS = "IO::Async::Loop::" . ( IO::Async::OS->LOOP_BUILTIN_CLASSES )[0];
+
+$loop = IO::Async::Loop->new;
+
+isa_ok( $loop, $LOOPCLASS, 'Magic constructor in default mode' ) or
+ diag( 'ref($loop) is ' . ref $loop );
+
+is( IO::Async::Loop->new, $loop, 'IO::Async::Loop->new again yields same loop' );
+
+{
+ local $ENV{IO_ASYNC_LOOP} = "t::StupidLoop";
+ undef $IO::Async::Loop::ONE_TRUE_LOOP;
+
+ $loop = IO::Async::Loop->new;
+
+ isa_ok( $loop, "t::StupidLoop", 'Magic constructor obeys $ENV{IO_ASYNC_LOOP}' );
+}
+
+{
+ local $IO::Async::Loop::LOOP = "t::StupidLoop";
+ undef $IO::Async::Loop::ONE_TRUE_LOOP;
+
+ $loop = IO::Async::Loop->new;
+
+ isa_ok( $loop, "t::StupidLoop", 'Magic constructor obeys $IO::Async::Loop::LOOP' );
+}
+
+{
+ local $IO::Async::Loop::LOOP = "Select";
+ undef $IO::Async::Loop::ONE_TRUE_LOOP;
+
+ $loop = IO::Async::Loop->new;
+
+ isa_ok( $loop, "IO::Async::Loop::Select", 'Magic constructor expands unqualified package names' );
+}
+
+done_testing;
diff --git a/t/04notifier.t b/t/04notifier.t
new file mode 100644
index 0000000..8016583
--- /dev/null
+++ b/t/04notifier.t
@@ -0,0 +1,194 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+use Test::Fatal;
+use Test::Refcount;
+
+use IO::Async::Notifier;
+
+{
+ my $notifier = IO::Async::Notifier->new(
+ notifier_name => "test1",
+ );
+
+ ok( defined $notifier, '$notifier defined' );
+ isa_ok( $notifier, "IO::Async::Notifier", '$notifier isa IO::Async::Notifier' );
+
+ is_oneref( $notifier, '$notifier has refcount 1 initially' );
+
+ is( $notifier->notifier_name, "test1", '$notifier->notifier_name' );
+
+ ok( !exception { $notifier->configure; },
+ '$notifier->configure no params succeeds' );
+
+ ok( exception { $notifier->configure( oranges => 1 ) },
+ '$notifier->configure an unknown parameter fails' );
+
+ my %other;
+ no warnings 'redefine';
+ local *IO::Async::Notifier::configure_unknown = sub {
+ shift;
+ %other = @_;
+ };
+
+ ok( !exception { $notifier->configure( oranges => 3 ) },
+ '$notifier->configure with configure_unknown succeeds' );
+
+ is_deeply( \%other, { oranges => 3 }, '%other after configure_unknown' );
+}
+
+# weaseling
+{
+ my $notifier = IO::Async::Notifier->new;
+
+ my @args;
+ my $mref = $notifier->_capture_weakself( sub { @args = @_ } );
+
+ is_oneref( $notifier, '$notifier has refcount 1 after _capture_weakself' );
+
+ $mref->( 123 );
+ is_deeply( \@args, [ $notifier, 123 ], '@args after invoking $mref' );
+
+ my @callstack;
+ $notifier->_capture_weakself( sub {
+ my $level = 0;
+ push @callstack, [ (caller $level++)[0,3] ] while defined caller $level;
+ } )->();
+
+ is_deeply( \@callstack,
+ [ [ "main", "main::__ANON__" ] ],
+ 'trampoline does not appear in _capture_weakself callstack' );
+
+ undef @args;
+
+ $mref = $notifier->_replace_weakself( sub { @args = @_ } );
+
+ is_oneref( $notifier, '$notifier has refcount 1 after _replace_weakself' );
+
+ my $outerself = bless [], "OtherClass";
+ $mref->( $outerself, 456 );
+ is_deeply( \@args, [ $notifier, 456 ], '@args after invoking replacer $mref' );
+
+ isa_ok( $outerself, "OtherClass", '$outerself unchanged' );
+
+ ok( exception { $notifier->_capture_weakself( 'cannotdo' ) },
+ '$notifier->_capture_weakself on unknown method name fails' );
+}
+
+# Subclass
+{
+ my @subargs;
+ {
+ package TestNotifier;
+ use base qw( IO::Async::Notifier );
+
+ sub frobnicate { @subargs = @_ }
+ }
+
+ my $subn = TestNotifier->new;
+
+ my $mref = $subn->_capture_weakself( 'frobnicate' );
+
+ is_oneref( $subn, '$subn has refcount 1 after _capture_weakself on named method' );
+
+ $mref->( 456 );
+ is_deeply( \@subargs, [ $subn, 456 ], '@subargs after invoking $mref on named method' );
+
+ undef @subargs;
+
+ # Method capture
+ {
+ my @newargs;
+
+ no warnings 'redefine';
+ local *TestNotifier::frobnicate = sub { @newargs = @_; };
+
+ $mref->( 321 );
+
+ is_deeply( \@subargs, [], '@subargs empty after TestNotifier::frobnicate replacement' );
+ is_deeply( \@newargs, [ $subn, 321 ], '@newargs after TestNotifier::frobnicate replacement' );
+ }
+
+ undef @subargs;
+
+ $subn->invoke_event( 'frobnicate', 78 );
+ is_deeply( \@subargs, [ $subn, 78 ], '@subargs after ->invoke_event' );
+
+ undef @subargs;
+
+ is_deeply( $subn->maybe_invoke_event( 'frobnicate', 'a'..'c' ),
+ [ $subn, 'a'..'c' ],
+ 'return value from ->maybe_invoke_event' );
+
+ is( $subn->maybe_invoke_event( 'mangle' ), undef, 'return value from ->maybe_invoke_event on missing event' );
+
+ undef @subargs;
+
+ my $cb = $subn->make_event_cb( 'frobnicate' );
+
+ is( ref $cb, "CODE", '->make_event_cb returns a CODE reference' );
+ is_oneref( $subn, '$subn has refcount 1 after ->make_event_cb' );
+
+ $cb->( 90 );
+ is_deeply( \@subargs, [ $subn, 90 ], '@subargs after ->make_event_cb->()' );
+
+ isa_ok( $subn->maybe_make_event_cb( 'frobnicate' ), "CODE", '->maybe_make_event_cb yields CODE ref' );
+ is( $subn->maybe_make_event_cb( 'mangle' ), undef, '->maybe_make_event_cb on missing event yields undef' );
+
+ undef @subargs;
+
+ is_oneref( $subn, '$subn has refcount 1 finally' );
+}
+
+# parent/child
+{
+ my $parent = IO::Async::Notifier->new;
+ my $child = IO::Async::Notifier->new;
+
+ is_oneref( $parent, '$parent has refcount 1 initially' );
+ is_oneref( $child, '$child has refcount 1 initially' );
+
+ $parent->add_child( $child );
+
+ is( $child->parent, $parent, '$child->parent is $parent' );
+ is_deeply( [ $parent->children ], [ $child ], '$parent->children' );
+
+ is_oneref( $parent, '$parent has refcount 1 after add_child' );
+ is_refcount( $child, 2, '$child has refcount 2 after add_child' );
+
+ ok( exception { $parent->add_child( $child ) }, 'Adding child again fails' );
+
+ $parent->remove_child( $child );
+
+ is_oneref( $child, '$child has refcount 1 after remove_child' );
+ is_deeply( [ $parent->children ], [], '$parent->children now empty' );
+}
+
+# invoke_error
+{
+ my $parent = IO::Async::Notifier->new;
+ my $child = IO::Async::Notifier->new;
+
+ $parent->add_child( $child );
+
+ # invoke_error no handler
+ ok( exception { $parent->invoke_error( "It went wrong", wrong => ) },
+ 'Exception thrown from ->invoke_error with no handler' );
+
+ # invoke_error handler
+ my $err;
+ $parent->configure( on_error => sub { $err = $_[1] } );
+
+ ok( !exception { $parent->invoke_error( "It's still wrong", wrong => ) },
+ 'Exception not thrown from ->invoke_error with handler' );
+ is( $err, "It's still wrong", '$message to on_error' );
+
+ ok( !exception { $child->invoke_error( "Wrong on child", wrong => ) },
+ 'Exception not thrown from ->invoke_error on child' );
+ is( $err, "Wrong on child", '$message to parent on_error' );
+}
+
+done_testing;
diff --git a/t/05notifier-loop.t b/t/05notifier-loop.t
new file mode 100644
index 0000000..ceaea6e
--- /dev/null
+++ b/t/05notifier-loop.t
@@ -0,0 +1,129 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+use Test::Fatal;
+use Test::Refcount;
+
+use IO::Async::Notifier;
+
+use IO::Async::Loop;
+
+my $loop = IO::Async::Loop->new;
+is_refcount( $loop, 2, '$loop has refcount 2 initially' );
+
+{
+ package TestNotifier;
+ use base qw( IO::Async::Notifier );
+
+ sub new
+ {
+ my $self = shift->SUPER::new;
+ ( $self->{varref} ) = @_;
+ return $self;
+ }
+
+ sub _add_to_loop
+ {
+ my $self = shift;
+ ${ $self->{varref} } = 1;
+ }
+
+ sub _remove_from_loop
+ {
+ my $self = shift;
+ ${ $self->{varref} } = 0;
+ }
+}
+
+# $loop->add
+{
+ my $notifier = TestNotifier->new( \my $in_loop );
+
+ is_deeply( [ $loop->notifiers ],
+ [],
+ '$loop->notifiers empty' );
+ is( $notifier->loop, undef, 'loop undef' );
+
+ $loop->add( $notifier );
+
+ is_refcount( $loop, 2, '$loop has refcount 2 adding Notifier' );
+ is_refcount( $notifier, 2, '$notifier has refcount 2 after adding to Loop' );
+
+ is( $notifier->loop, $loop, 'loop $loop' );
+
+ is_deeply( [ $loop->notifiers ],
+ [ $notifier ],
+ '$loop->notifiers contains new Notifier' );
+
+ ok( $in_loop, '_add_to_loop called' );
+
+ ok( exception { $loop->add( $notifier ) }, 'adding again produces error' );
+
+ $loop->remove( $notifier );
+
+ is( $notifier->loop, undef, '$notifier->loop is undef' );
+
+ is_deeply( [ $loop->notifiers ],
+ [],
+ '$loop->notifiers empty once more' );
+
+ ok( !$in_loop, '_remove_from_loop called' );
+
+ is_oneref( $notifier, '$notifier has refcount 1 finally' );
+}
+
+# parent/child in Loop
+{
+ my $parent = TestNotifier->new( \my $parent_in_loop );
+ my $child = TestNotifier->new( \my $child_in_loop );
+
+ $loop->add( $parent );
+
+ $parent->add_child( $child );
+
+ is_refcount( $child, 3, '$child has refcount 3 after add_child within loop' );
+
+ is( $parent->loop, $loop, '$parent->loop is $loop' );
+ is( $child->loop, $loop, '$child->loop is $loop' );
+
+ ok( $parent_in_loop, '$parent now in loop' );
+ ok( $child_in_loop, '$child now in loop' );
+
+ ok( exception { $loop->remove( $child ) }, 'Directly removing a child from the loop fails' );
+
+ $loop->remove( $parent );
+
+ is_deeply( [ $parent->children ], [ $child ], '$parent->children after $loop->remove' );
+
+ is_oneref( $parent, '$parent has refcount 1 after removal from loop' );
+ is_refcount( $child, 2, '$child has refcount 2 after removal of parent from loop' );
+
+ is( $parent->loop, undef, '$parent->loop is undef' );
+ is( $child->loop, undef, '$child->loop is undef' );
+
+ ok( !$parent_in_loop, '$parent no longer in loop' );
+ ok( !$child_in_loop, '$child no longer in loop' );
+
+ ok( exception { $loop->add( $child ) }, 'Directly adding a child to the loop fails' );
+
+ $loop->add( $parent );
+
+ is( $child->loop, $loop, '$child->loop is $loop after remove/add parent' );
+
+ ok( $parent_in_loop, '$parent now in loop' );
+ ok( $child_in_loop, '$child now in loop' );
+
+ $loop->remove( $parent );
+
+ $parent->remove_child( $child );
+
+ is_oneref( $parent, '$parent has refcount 1 finally' );
+ is_oneref( $child, '$child has refcount 1 finally' );
+}
+
+is_refcount( $loop, 2, '$loop has refcount 2 finally' );
+
+done_testing;
diff --git a/t/06notifier-mixin.t b/t/06notifier-mixin.t
new file mode 100644
index 0000000..7554fc4
--- /dev/null
+++ b/t/06notifier-mixin.t
@@ -0,0 +1,50 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+use Test::Refcount;
+
+use IO::Async::Loop;
+
+my $loop = IO::Async::Loop->new;
+
+is_refcount( $loop, 2, '$loop has refcount 2 initially' );
+
+my $notifier = SomeEventSource::Async->new;
+my $in_loop;
+
+isa_ok( $notifier, "SomeEventSource", '$notifier isa SomeEventSource' );
+isa_ok( $notifier, "IO::Async::Notifier", '$notifier isa IO::Async::Notifier' );
+
+$loop->add( $notifier );
+
+is_refcount( $loop, 2, '$loop has refcount 2 adding Notifier' );
+is_refcount( $notifier, 2, '$notifier has refcount 2 after adding to Loop' );
+
+is( $notifier->loop, $loop, 'loop $loop' );
+
+ok( $in_loop, 'SomeEventSource::Async added to Loop' );
+
+$loop->remove( $notifier );
+
+is( $notifier->loop, undef, '$notifier->loop is undef' );
+
+ok( !$in_loop, 'SomeEventSource::Async removed from Loop' );
+
+done_testing;
+
+package SomeEventSource;
+
+sub new
+{
+ my $class = shift;
+ return bless {}, $class;
+}
+
+package SomeEventSource::Async;
+use base qw( SomeEventSource IO::Async::Notifier );
+
+sub _add_to_loop { $in_loop = 1 }
+sub _remove_from_loop { $in_loop = 0 }
diff --git a/t/07notifier-future.t b/t/07notifier-future.t
new file mode 100644
index 0000000..066fbb9
--- /dev/null
+++ b/t/07notifier-future.t
@@ -0,0 +1,58 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+use Test::Fatal;
+use Test::Refcount;
+
+use IO::Async::Notifier;
+use Future;
+
+my ( $err, $name, @detail );
+my $notifier = IO::Async::Notifier->new(
+ on_error => sub {
+ ( undef, $err, $name, @detail ) = @_;
+ },
+);
+
+# done
+{
+ my $f = Future->new;
+
+ $notifier->adopt_future( $f );
+
+ is_refcount( $f, 2, '$f has refcount 2 after ->adopt_future' );
+ is_oneref( $notifier, '$notifier still has refcount 1 after ->adopt_future' );
+
+ $f->done( "result" );
+
+ is_refcount( $f, 1, '$f has refcount 1 after $f->done' );
+}
+
+# fail
+{
+ my $f = Future->new;
+
+ $notifier->adopt_future( $f );
+
+ $f->fail( "It failed", name => 1, 2, 3 );
+
+ is( $err, "It failed", '$err after $f->fail' );
+ is( $name, "name", '$name after $f->fail' );
+ is_deeply( \@detail, [ 1, 2, 3 ], '@detail after $f->fail' );
+
+ is_refcount( $f, 1, '$f has refcount 1 after $f->fail' );
+
+ undef $err;
+
+ $f = Future->new;
+ $notifier->adopt_future( $f->else_done() );
+
+ $f->fail( "Not captured" );
+
+ ok( !defined $err, '$err not defined after ->else_done suppressed failure' );
+}
+
+done_testing;
diff --git a/t/10loop-poll-io.t b/t/10loop-poll-io.t
new file mode 100644
index 0000000..8c34ffa
--- /dev/null
+++ b/t/10loop-poll-io.t
@@ -0,0 +1,7 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::LoopTests;
+run_tests( 'IO::Async::Loop::Poll', 'io' );
diff --git a/t/10loop-select-io.t b/t/10loop-select-io.t
new file mode 100644
index 0000000..b3e3916
--- /dev/null
+++ b/t/10loop-select-io.t
@@ -0,0 +1,7 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::LoopTests;
+run_tests( 'IO::Async::Loop::Select', 'io' );
diff --git a/t/11loop-poll-timer.t b/t/11loop-poll-timer.t
new file mode 100644
index 0000000..748ba2a
--- /dev/null
+++ b/t/11loop-poll-timer.t
@@ -0,0 +1,7 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::LoopTests;
+run_tests( 'IO::Async::Loop::Poll', 'timer' );
diff --git a/t/11loop-select-timer.t b/t/11loop-select-timer.t
new file mode 100644
index 0000000..314bd38
--- /dev/null
+++ b/t/11loop-select-timer.t
@@ -0,0 +1,7 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::LoopTests;
+run_tests( 'IO::Async::Loop::Select', 'timer' );
diff --git a/t/12loop-poll-signal.t b/t/12loop-poll-signal.t
new file mode 100644
index 0000000..4c8ef24
--- /dev/null
+++ b/t/12loop-poll-signal.t
@@ -0,0 +1,9 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::LoopTests;
+Test::More::plan skip_all => "This OS does not have signals" unless IO::Async::OS->HAVE_SIGNALS;
+
+run_tests( 'IO::Async::Loop::Poll', 'signal' );
diff --git a/t/12loop-select-signal.t b/t/12loop-select-signal.t
new file mode 100644
index 0000000..66a6c91
--- /dev/null
+++ b/t/12loop-select-signal.t
@@ -0,0 +1,9 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::LoopTests;
+Test::More::plan skip_all => "This OS does not have signals" unless IO::Async::OS->HAVE_SIGNALS;
+
+run_tests( 'IO::Async::Loop::Select', 'signal' );
diff --git a/t/13loop-poll-idle.t b/t/13loop-poll-idle.t
new file mode 100644
index 0000000..3ce457b
--- /dev/null
+++ b/t/13loop-poll-idle.t
@@ -0,0 +1,7 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::LoopTests;
+run_tests( 'IO::Async::Loop::Poll', 'idle' );
diff --git a/t/13loop-select-idle.t b/t/13loop-select-idle.t
new file mode 100644
index 0000000..8cea7a7
--- /dev/null
+++ b/t/13loop-select-idle.t
@@ -0,0 +1,7 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::LoopTests;
+run_tests( 'IO::Async::Loop::Select', 'idle' );
diff --git a/t/14loop-poll-child.t b/t/14loop-poll-child.t
new file mode 100644
index 0000000..5337166
--- /dev/null
+++ b/t/14loop-poll-child.t
@@ -0,0 +1,7 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::LoopTests;
+run_tests( 'IO::Async::Loop::Poll', 'child' );
diff --git a/t/14loop-select-child.t b/t/14loop-select-child.t
new file mode 100644
index 0000000..8e31e23
--- /dev/null
+++ b/t/14loop-select-child.t
@@ -0,0 +1,7 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::LoopTests;
+run_tests( 'IO::Async::Loop::Select', 'child' );
diff --git a/t/15loop-poll-control.t b/t/15loop-poll-control.t
new file mode 100644
index 0000000..74839dd
--- /dev/null
+++ b/t/15loop-poll-control.t
@@ -0,0 +1,7 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::LoopTests;
+run_tests( 'IO::Async::Loop::Poll', 'control' );
diff --git a/t/15loop-select-control.t b/t/15loop-select-control.t
new file mode 100644
index 0000000..ff62634
--- /dev/null
+++ b/t/15loop-select-control.t
@@ -0,0 +1,7 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::LoopTests;
+run_tests( 'IO::Async::Loop::Select', 'control' );
diff --git a/t/18loop-poll-legacy.t b/t/18loop-poll-legacy.t
new file mode 100644
index 0000000..3c20dfb
--- /dev/null
+++ b/t/18loop-poll-legacy.t
@@ -0,0 +1,95 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+
+use IO::Poll;
+
+use IO::Async::OS;
+
+use IO::Async::Loop::Poll;
+
+my $poll = IO::Poll->new;
+my $loop = IO::Async::Loop::Poll->new( poll => $poll );
+
+my ( $S1, $S2 ) = IO::Async::OS->socketpair or die "Cannot create socket pair - $!";
+
+# Need sockets in nonblocking mode
+$S1->blocking( 0 );
+$S2->blocking( 0 );
+
+# Empty
+
+is_deeply( [ $poll->handles ], [], '$poll->handles empty initially' );
+
+# watch_io
+
+my $readready = 0;
+$loop->watch_io(
+ handle => $S1,
+ on_read_ready => sub { $readready = 1 },
+);
+
+is_deeply( [ $poll->handles ], [ $S1 ], '$poll->handles after watch_io read_ready' );
+
+$S2->syswrite( "data\n" );
+
+# We should still wait a little while even thought we expect to be ready
+# immediately, because talking to ourself with 0 poll timeout is a race
+# condition - we can still race with the kernel.
+
+$poll->poll( 0.1 );
+
+is( $readready, 0, '$readready before post_poll' );
+$loop->post_poll;
+is( $readready, 1, '$readready after post_poll' );
+
+# Ready $S1 to clear the data
+$S1->getline; # ignore return
+
+$loop->unwatch_io(
+ handle => $S1,
+ on_read_ready => 1,
+);
+
+is_deeply( [ $poll->handles ], [], '$poll->handles empty after unwatch_io read_ready' );
+
+my $writeready = 0;
+$loop->watch_io(
+ handle => $S1,
+ on_write_ready => sub { $writeready = 1 },
+);
+
+is_deeply( [ $poll->handles ], [ $S1 ], '$poll->handles after watch_io write_ready' );
+
+$poll->poll( 0.1 );
+
+is( $writeready, 0, '$writeready before post_poll' );
+$loop->post_poll;
+is( $writeready, 1, '$writeready after post_poll' );
+
+$loop->unwatch_io(
+ handle => $S1,
+ on_write_ready => 1,
+);
+
+is_deeply( [ $poll->handles ], [], '$poll->handles empty after unwatch_io write_ready' );
+
+# Removal is clean (tests for workaround to bug in IO::Poll version 0.05)
+
+my ( $P1, $P2 ) = IO::Async::OS->pipepair or die "Cannot pipepair - $!";
+
+# Just to make the loop non-empty
+$loop->watch_io( handle => $P2, on_read_ready => sub {} );
+
+$loop->watch_io( handle => \*STDOUT, on_write_ready => sub {} );
+
+is( scalar $poll->handles, 2, '$poll->handles before removal in clean removal test' );
+
+$loop->unwatch_io( handle => \*STDOUT, on_write_ready => 1 );
+
+is( scalar $poll->handles, 1, '$poll->handles after removal in clean removal test' );
+
+done_testing;
diff --git a/t/18loop-select-legacy.t b/t/18loop-select-legacy.t
new file mode 100644
index 0000000..d5796b0
--- /dev/null
+++ b/t/18loop-select-legacy.t
@@ -0,0 +1,152 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+
+use Time::HiRes qw( time );
+
+use IO::Async::Loop::Select;
+
+use IO::Async::OS;
+
+use constant AUT => $ENV{TEST_QUICK_TIMERS} ? 0.1 : 1;
+
+my $loop = IO::Async::Loop::Select->new;
+
+my ( $S1, $S2 ) = IO::Async::OS->socketpair or die "Cannot create socket pair - $!";
+
+# Need sockets in nonblocking mode
+$S1->blocking( 0 );
+$S2->blocking( 0 );
+
+my $testvec = '';
+vec( $testvec, $S1->fileno, 1 ) = 1;
+
+my ( $rvec, $wvec, $evec ) = ('') x 3;
+my $timeout;
+
+# Empty
+
+$loop->pre_select( \$rvec, \$wvec, \$evec, \$timeout );
+is( $rvec, '', '$rvec idling pre_select' );
+is( $wvec, '', '$wvec idling pre_select' );
+is( $evec, '', '$evec idling pre_select' );
+is( $timeout, undef, '$timeout idling pre_select' );
+
+# watch_io
+
+my $readready = 0;
+$loop->watch_io(
+ handle => $S1,
+ on_read_ready => sub { $readready = 1 },
+);
+
+$loop->pre_select( \$rvec, \$wvec, \$evec, \$timeout );
+
+is( $rvec, $testvec, '$rvec readready pre_select' );
+is( $wvec, '', '$wvec readready pre_select' );
+is( $evec, '', '$evec readready pre_select' );
+is( $timeout, undef, '$timeout readready pre_select' );
+
+is( $readready, 0, '$readready readready pre_select' );
+
+$rvec = $testvec;
+$wvec = '';
+$evec = '';
+
+$loop->post_select( $rvec, $wvec, $evec );
+
+is( $readready, 1, '$readready readready post_select' );
+
+$loop->unwatch_io(
+ handle => $S1,
+ on_read_ready => 1,
+);
+
+my $writeready = 0;
+$loop->watch_io(
+ handle => $S1,
+ on_write_ready => sub { $writeready = 1 },
+);
+
+$loop->pre_select( \$rvec, \$wvec, \$evec, \$timeout );
+
+is( $rvec, $testvec, '$rvec writeready pre_select' );
+is( $wvec, $testvec, '$wvec writeready pre_select' );
+is( $evec, IO::Async::OS->HAVE_SELECT_CONNECT_EVEC ? $testvec : '', '$evec writeready pre_select' );
+is( $timeout, undef, '$timeout writeready pre_select' );
+
+is( $writeready, 0, '$writeready writeready pre_select' );
+
+$rvec = '';
+$wvec = $testvec;
+$evec = '';
+
+$loop->post_select( $rvec, $wvec, $evec );
+
+is( $writeready, 1, '$writeready writeready post_select' );
+
+$loop->unwatch_io(
+ handle => $S1,
+ on_write_ready => 1,
+);
+
+# watch_time
+
+$rvec = $wvec = $evec = '';
+$timeout = 5 * AUT;
+
+$loop->pre_select( \$rvec, \$wvec, \$evec, \$timeout );
+is( $timeout, 5 * AUT, '$timeout idling pre_select with timeout' );
+
+my $done = 0;
+$loop->watch_time( after => 2 * AUT, code => sub { $done = 1; } );
+
+$loop->pre_select( \$rvec, \$wvec, \$evec, \$timeout );
+cmp_ok( $timeout/AUT, '>', 1.7, '$timeout while timer waiting pre_select at least 1.7' );
+cmp_ok( $timeout/AUT, '<', 2.5, '$timeout while timer waiting pre_select at least 2.5' );
+
+my ( $now, $took );
+
+$now = time;
+select( $rvec, $wvec, $evec, $timeout );
+$took = (time - $now) / AUT;
+
+cmp_ok( $took, '>', 1.7, 'loop_once(5) while waiting for timer takes at least 1.7 seconds' );
+cmp_ok( $took, '<', 10, 'loop_once(5) while waiting for timer no more than 10 seconds' );
+if( $took > 2.5 ) {
+ diag( "took more than 2.5 seconds to select(2).\n" .
+ "This is not itself a bug, and may just be an indication of a busy testing machine" );
+}
+
+$loop->post_select( $rvec, $evec, $wvec );
+
+# select might have returned just a little early, such that the TimerQueue
+# doesn't think anything is ready yet. We need to handle that case.
+while( !$done ) {
+ die "It should have been ready by now" if( time - $now > 5 * AUT );
+
+ $timeout = 0.1 * AUT;
+
+ $loop->pre_select( \$rvec, \$wvec, \$evec, \$timeout );
+ select( $rvec, $wvec, $evec, $timeout );
+ $loop->post_select( $rvec, $evec, $wvec );
+}
+
+is( $done, 1, '$done after post_select while waiting for timer' );
+
+my $id = $loop->watch_time( after => 1 * AUT, code => sub { $done = 2; } );
+$loop->unwatch_time( $id );
+
+$done = 0;
+$now = time;
+
+$loop->pre_select( \$rvec, \$wvec, \$evec, \$timeout );
+select( $rvec, $wvec, $evec, 1.5 * AUT );
+$loop->post_select( $rvec, $evec, $wvec );
+
+is( $done, 0, '$done still 0 before cancelled timeout' );
+
+done_testing;
diff --git a/t/19loop-future.t b/t/19loop-future.t
new file mode 100644
index 0000000..45ab441
--- /dev/null
+++ b/t/19loop-future.t
@@ -0,0 +1,107 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+use Test::Identity;
+use t::TimeAbout;
+
+use IO::Async::Loop;
+
+use Future;
+use IO::Async::Future;
+
+use constant AUT => $ENV{TEST_QUICK_TIMERS} ? 0.1 : 1;
+
+my $loop = IO::Async::Loop->new_builtin;
+
+{
+ my $future = Future->new;
+
+ $loop->later( sub { $future->done( "result" ) } );
+
+ my $ret = $loop->await( $future );
+ identical( $ret, $future, '$loop->await( $future ) returns $future' );
+
+ is_deeply( [ $future->get ], [ "result" ], '$future->get' );
+}
+
+{
+ my @futures = map { Future->new } 0 .. 2;
+
+ do { my $id = $_; $loop->later( sub { $futures[$id]->done } ) } for 0 .. 2;
+
+ $loop->await_all( @futures );
+
+ ok( 1, '$loop->await_all' );
+ ok( $futures[$_]->is_ready, "future $_ ready" ) for 0 .. 2;
+}
+
+{
+ my $future = IO::Async::Future->new( $loop );
+
+ identical( $future->loop, $loop, '$future->loop yields $loop' );
+
+ $loop->later( sub { $future->done( "result" ) } );
+
+ is_deeply( [ $future->get ], [ "result" ], '$future->get on IO::Async::Future' );
+}
+
+{
+ my $future = $loop->new_future;
+
+ $loop->later( sub { $future->done( "result" ) } );
+
+ is_deeply( [ $future->get ], [ "result" ], '$future->get on IO::Async::Future from $loop->new_future' );
+}
+
+# done_later
+{
+ my $future = $loop->new_future;
+
+ identical( $future->done_later( "deferred result" ), $future, '->done_later returns $future' );
+ ok( !$future->is_ready, '$future not yet ready after ->done_later' );
+
+ is_deeply( [ $future->get ], [ "deferred result" ], '$future now ready after ->get' );
+}
+
+# fail_later
+{
+ my $future = $loop->new_future;
+
+ identical( $future->fail_later( "deferred exception\n" ), $future, '->fail_later returns $future' );
+ ok( !$future->is_ready, '$future not yet ready after ->fail_later' );
+
+ $loop->await( $future );
+
+ is_deeply( [ $future->failure ], [ "deferred exception\n" ], '$future now ready after $loop->await' );
+}
+
+# delay_future
+{
+ my $future = $loop->delay_future( after => 1 * AUT );
+
+ time_about( sub { $loop->await( $future ) }, 1, '->delay_future is ready' );
+
+ ok( $future->is_ready, '$future is ready from delay_future' );
+ is_deeply( [ $future->get ], [], '$future->get returns empty list on delay_future' );
+
+ # Check that ->cancel does not crash
+ $loop->delay_future( after => 1 * AUT )->cancel;
+}
+
+# timeout_future
+{
+ my $future = $loop->timeout_future( after => 1 * AUT );
+
+ time_about( sub { $loop->await( $future ) }, 1, '->timeout_future is ready' );
+
+ ok( $future->is_ready, '$future is ready from timeout_future' );
+ is( scalar $future->failure, "Timeout", '$future failed with "Timeout" for timeout_future' );
+
+ # Check that ->cancel does not crash
+ $loop->timeout_future( after => 1 * AUT )->cancel;
+}
+
+done_testing;
diff --git a/t/19test.t b/t/19test.t
new file mode 100644
index 0000000..cc24d45
--- /dev/null
+++ b/t/19test.t
@@ -0,0 +1,69 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+use Test::Refcount;
+use IO::Async::Test;
+
+use IO::Async::OS;
+
+use IO::Async::Loop;
+
+my $loop = IO::Async::Loop->new_builtin;
+
+is_refcount( $loop, 2, '$loop has refcount 2 initially' );
+
+testing_loop( $loop );
+
+is_refcount( $loop, 3, '$loop has refcount 3 after adding to IO::Async::Test' );
+
+my ( $S1, $S2 ) = IO::Async::OS->socketpair or die "Cannot create socket pair - $!";
+
+my $readbuffer = "";
+
+$loop->watch_io(
+ handle => $S1,
+ on_read_ready => sub {
+ $S1->sysread( $readbuffer, 8192, length $readbuffer ) or die "Test failed early";
+ },
+);
+
+# This is just a token "does it run once?" test. A test of a test script.
+# Mmmmmm. Meta-testing.
+# Coming up with a proper test that would guarantee multiple loop_once
+# cycles, etc.. is difficult. TODO for later I feel.
+# In any case, the wait_for function is effectively tested to death in later
+# test scripts which use it. If it fails to work, they'd notice it.
+
+$S2->syswrite( "A line\n" );
+
+wait_for { $readbuffer =~ m/\n/ };
+
+is( $readbuffer, "A line\n", 'Single-wait' );
+
+$loop->unwatch_io(
+ handle => $S1,
+ on_read_ready => 1,
+);
+
+# Now the automatic version
+
+$readbuffer = "";
+
+$S2->syswrite( "Another line\n" );
+
+wait_for_stream { $readbuffer =~ m/\n/ } $S1 => $readbuffer;
+
+is( $readbuffer, "Another line\n", 'Automatic stream read wait' );
+
+$readbuffer = "";
+
+$S2->syswrite( "Some dynamic data\n" );
+
+wait_for_stream { $readbuffer =~ m/\n/ } $S1 => sub { $readbuffer .= shift };
+
+is( $readbuffer, "Some dynamic data\n" );
+
+done_testing;
diff --git a/t/20handle.t b/t/20handle.t
new file mode 100644
index 0000000..194a621
--- /dev/null
+++ b/t/20handle.t
@@ -0,0 +1,422 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+use Test::Fatal;
+use Test::Identity;
+use Test::Refcount;
+
+use IO::Async::Loop;
+
+use IO::Async::Handle;
+
+use IO::Async::OS;
+
+use Socket qw( AF_INET SOCK_STREAM SOCK_DGRAM SO_TYPE unpack_sockaddr_in );
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+sub mkhandles
+{
+ my ( $S1, $S2 ) = IO::Async::OS->socketpair or die "Cannot create socket pair - $!";
+
+ # Need sockets in nonblocking mode
+ $S1->blocking( 0 );
+ $S2->blocking( 0 );
+
+ return ( $S1, $S2 );
+}
+
+ok( exception { IO::Async::Handle->new( handle => "Hello" ) }, 'Not a filehandle' );
+
+# Read readiness
+{
+ my ( $S1, $S2 ) = mkhandles;
+ my $fd1 = $S1->fileno;
+
+ my $readready = 0;
+ my @rrargs;
+
+ my $handle = IO::Async::Handle->new(
+ read_handle => $S1,
+ on_read_ready => sub { @rrargs = @_; $readready = 1 },
+ );
+
+ ok( defined $handle, '$handle defined' );
+ isa_ok( $handle, "IO::Async::Handle", '$handle isa IO::Async::Handle' );
+
+ is( $handle->notifier_name, "r=$fd1", '$handle->notifier_name for read_handle' );
+
+ is_oneref( $handle, '$handle has refcount 1 initially' );
+
+ is( $handle->read_handle, $S1, '->read_handle returns S1' );
+ is( $handle->read_fileno, $S1->fileno, '->read_fileno returns fileno(S1)' );
+
+ is( $handle->write_handle, undef, '->write_handle returns undef' );
+
+ ok( $handle->want_readready, 'want_readready true' );
+
+ $loop->add( $handle );
+
+ is_refcount( $handle, 2, '$handle has refcount 2 after adding to Loop' );
+
+ $loop->loop_once( 0.1 ); # nothing happens
+
+ is( $readready, 0, '$readready while idle' );
+
+ $S2->syswrite( "data\n" );
+
+ wait_for { $readready };
+
+ is( $readready, 1, '$readready while readable' );
+ is_deeply( \@rrargs, [ $handle ], 'on_read_ready args while readable' );
+
+ $S1->getline; # ignore return
+
+ $readready = 0;
+ my $new_readready = 0;
+
+ $handle->configure( on_read_ready => sub { $new_readready = 1 } );
+
+ $loop->loop_once( 0.1 ); # nothing happens
+
+ is( $readready, 0, '$readready while idle after on_read_ready replace' );
+ is( $new_readready, 0, '$new_readready while idle after on_read_ready replace' );
+
+ $S2->syswrite( "data\n" );
+
+ wait_for { $new_readready };
+
+ is( $readready, 0, '$readready while readable after on_read_ready replace' );
+ is( $new_readready, 1, '$new_readready while readable after on_read_ready replace' );
+
+ $S1->getline; # ignore return
+
+ ok( exception { $handle->want_writeready( 1 ); },
+ 'setting want_writeready with write_handle == undef dies' );
+ ok( !$handle->want_writeready, 'wantwriteready write_handle == undef false' );
+
+ undef @rrargs;
+
+ is_refcount( $handle, 2, '$handle has refcount 2 before removing from Loop' );
+
+ $loop->remove( $handle );
+
+ is_oneref( $handle, '$handle has refcount 1 finally' );
+}
+
+# Write readiness
+{
+ my ( $S1, $S2 ) = mkhandles;
+ my $fd1 = $S1->fileno;
+
+ my $writeready = 0;
+ my @wrargs;
+
+ my $handle = IO::Async::Handle->new(
+ write_handle => $S1,
+ on_write_ready => sub { @wrargs = @_; $writeready = 1 },
+ );
+
+ ok( defined $handle, '$handle defined' );
+ isa_ok( $handle, "IO::Async::Handle", '$handle isa IO::Async::Handle' );
+
+ is( $handle->notifier_name, "w=$fd1", '$handle->notifier_name for write_handle' );
+
+ is_oneref( $handle, '$handle has refcount 1 initially' );
+
+ is( $handle->write_handle, $S1, '->write_handle returns S1' );
+ is( $handle->write_fileno, $S1->fileno, '->write_fileno returns fileno(S1)' );
+
+ is( $handle->read_handle, undef, '->read_handle returns undef' );
+
+ ok( !$handle->want_writeready, 'want_writeready false' );
+
+ $loop->add( $handle );
+
+ is_refcount( $handle, 2, '$handle has refcount 2 after adding to Loop' );
+
+ $loop->loop_once( 0.1 ); # nothing happens
+
+ is( $writeready, 0, '$writeready while idle' );
+
+ $handle->want_writeready( 1 );
+
+ wait_for { $writeready };
+
+ is( $writeready, 1, '$writeready while writeable' );
+ is_deeply( \@wrargs, [ $handle ], 'on_write_ready args while writeable' );
+
+ $writeready = 0;
+ my $new_writeready = 0;
+
+ $handle->configure( on_write_ready => sub { $new_writeready = 1 } );
+
+ wait_for { $new_writeready };
+
+ is( $writeready, 0, '$writeready while writeable after on_write_ready replace' );
+ is( $new_writeready, 1, '$new_writeready while writeable after on_write_ready replace' );
+
+ undef @wrargs;
+
+ is_refcount( $handle, 2, '$handle has refcount 2 before removing from Loop' );
+
+ $loop->remove( $handle );
+
+ is_oneref( $handle, '$handle has refcount 1 finally' );
+}
+
+# Combined handle
+{
+ my ( $S1, $S2 ) = mkhandles;
+ my $fd1 = $S1->fileno;
+
+ my $handle = IO::Async::Handle->new(
+ handle => $S1,
+ on_read_ready => sub {},
+ on_write_ready => sub {},
+ );
+
+ is( $handle->read_handle, $S1, '->read_handle returns S1' );
+ is( $handle->write_handle, $S1, '->write_handle returns S1' );
+
+ is( $handle->notifier_name, "rw=$fd1", '$handle->notifier_name for handle' );
+}
+
+# Subclass
+my $sub_readready = 0;
+my $sub_writeready = 0;
+
+{
+ my ( $S1, $S2 ) = mkhandles;
+
+ my $handle = TestHandle->new(
+ handle => $S1,
+ );
+
+ ok( defined $handle, 'subclass $handle defined' );
+ isa_ok( $handle, "IO::Async::Handle", 'subclass $handle isa IO::Async::Handle' );
+
+ is_oneref( $handle, 'subclass $handle has refcount 1 initially' );
+
+ is( $handle->read_handle, $S1, 'subclass ->read_handle returns S1' );
+ is( $handle->write_handle, $S1, 'subclass ->write_handle returns S1' );
+
+ $loop->add( $handle );
+
+ is_refcount( $handle, 2, 'subclass $handle has refcount 2 after adding to Loop' );
+
+ $S2->syswrite( "data\n" );
+
+ wait_for { $sub_readready };
+
+ is( $sub_readready, 1, '$sub_readready while readable' );
+ is( $sub_writeready, 0, '$sub_writeready while readable' );
+
+ $S1->getline; # ignore return
+ $sub_readready = 0;
+
+ $handle->want_writeready( 1 );
+
+ wait_for { $sub_writeready };
+
+ is( $sub_readready, 0, '$sub_readready while writeable' );
+ is( $sub_writeready, 1, '$sub_writeready while writeable' );
+
+ $loop->remove( $handle );
+}
+
+# Close
+{
+ my ( $S1, $S2 ) = mkhandles;
+
+ my $closed = 0;
+
+ my $handle = IO::Async::Handle->new(
+ read_handle => $S1,
+ want_writeready => 0,
+ on_read_ready => sub {},
+ on_closed => sub { $closed = 1 },
+ );
+
+ $loop->add( $handle );
+
+ my $close_future = $handle->new_close_future;
+
+ my $closed_by_future;
+ $close_future->on_done( sub { $closed_by_future++ } );
+
+ $handle->close;
+
+ is( $closed, 1, '$closed after ->close' );
+
+ ok( $close_future->is_ready, '$close_future is now ready' );
+ is( $closed_by_future, 1, '$closed_by_future after ->close' );
+
+ # removed itself
+}
+
+# Close read/write
+{
+ my ( $Srd1, $Srd2 ) = mkhandles;
+ my ( $Swr1, $Swr2 ) = mkhandles;
+
+ local $SIG{PIPE} = "IGNORE";
+
+ my $readready = 0;
+ my $writeready = 0;
+
+ my $closed = 0;
+
+ my $handle = IO::Async::Handle->new(
+ read_handle => $Srd1,
+ write_handle => $Swr1,
+ on_read_ready => sub { $readready++ },
+ on_write_ready => sub { $writeready++ },
+ on_closed => sub { $closed++ },
+ want_writeready => 1,
+ );
+
+ $loop->add( $handle );
+
+ $handle->close_read;
+
+ wait_for { $writeready };
+ is( $writeready, 1, '$writeready after ->close_read' );
+
+ $handle->write_handle->syswrite( "Still works\n" );
+ is( $Swr2->getline, "Still works\n", 'write handle still works' );
+
+ is( $closed, 0, 'not $closed after ->close_read' );
+
+ is( $handle->loop, $loop, 'Handle still member of Loop after ->close_read' );
+
+ ( $Srd1, $Srd2 ) = mkhandles;
+
+ $handle->configure( read_handle => $Srd1 );
+
+ $handle->close_write;
+
+ $Srd2->syswrite( "Also works\n" );
+
+ wait_for { $readready };
+ is( $readready, 1, '$readready after ->close_write' );
+
+ is( $handle->read_handle->getline, "Also works\n", 'read handle still works' );
+ is( $Swr2->getline, undef, 'sysread from EOF write handle' );
+
+ is( $handle->loop, $loop, 'Handle still member of Loop after ->close_write' );
+
+ is( $closed, 0, 'not $closed after ->close_read' );
+
+ $handle->close_read;
+
+ is( $closed, 1, '$closed after ->close_read + ->close_write' );
+
+ is( $handle->loop, undef, '$handle no longer member of Loop' );
+}
+
+# Late-binding of handle
+{
+ my $readready;
+ my $writeready;
+
+ my $handle = IO::Async::Handle->new(
+ want_writeready => 0,
+ on_read_ready => sub { $readready = 1 },
+ on_write_ready => sub { $writeready = 1 },
+ );
+
+ ok( defined $handle, '$handle defined' );
+
+ ok( !defined $handle->read_handle, '->read_handle not defined' );
+ ok( !defined $handle->write_handle, '->write_handle not defined' );
+
+ is_oneref( $handle, '$handle latebound has refcount 1 initially' );
+
+ is( $handle->notifier_name, "no", '$handle->notifier_name for late bind before handles' );
+
+ $loop->add( $handle );
+
+ is_refcount( $handle, 2, '$handle latebound has refcount 2 after $loop->add' );
+
+ my ( $S1, $S2 ) = mkhandles;
+ my $fd1 = $S1->fileno;
+
+ $handle->set_handle( $S1 );
+
+ is( $handle->read_handle, $S1, '->read_handle now S1' );
+ is( $handle->write_handle, $S1, '->write_handle now S1' );
+
+ is_refcount( $handle, 2, '$handle latebound still has refcount 2 after set_handle' );
+
+ is( $handle->notifier_name, "rw=$fd1", '$handle->notifier_name for late bind after handles' );
+
+ $S2->syswrite( "readable" );
+
+ wait_for { $readready };
+ pass( '$handle latebound still invokes on_read_ready' );
+
+ $loop->remove( $handle );
+}
+
+# ->socket and ->bind
+{
+ my $handle = IO::Async::Handle->new( on_read_ready => sub {}, on_write_ready => sub {} );
+
+ $handle->socket( [ 'inet', 'stream', 0 ] );
+
+ ok( defined $handle->read_handle, '->socket sets handle' );
+
+ is( $handle->read_handle->sockdomain, AF_INET, 'handle->sockdomain is AF_INET' );
+ is( $handle->read_handle->sockopt(SO_TYPE), SOCK_STREAM, 'handle->socktype is SOCK_STREAM' );
+
+ $handle->bind( { family => "inet", socktype => "dgram" } )->get;
+
+ is( $handle->read_handle->sockopt(SO_TYPE), SOCK_DGRAM, 'handle->socktype is SOCK_DGRAM' );
+ # Not sure what port number but it should be nonzero
+ ok( ( unpack_sockaddr_in( $handle->read_handle->sockname ) )[0], 'handle->sockname has nonzero port' );
+}
+
+# Construction of IO::Handle from fileno
+{
+ my $handle = IO::Async::Handle->new(
+ read_fileno => 0,
+ on_read_ready => sub { },
+ );
+
+ ok( defined $handle->read_handle, '->new with read_fileno creates read_handle' );
+ is( $handle->read_handle->fileno, 0, '->fileno of read_handle' );
+
+ $handle = IO::Async::Handle->new(
+ write_fileno => 1,
+ on_write_ready => sub { },
+ );
+
+ ok( defined $handle->write_handle, '->new with write_fileno creates write_handle' );
+ is( $handle->write_handle->fileno, 1, '->fileno of write_handle' );
+
+ $handle = IO::Async::Handle->new(
+ read_fileno => 2,
+ write_fileno => 2,
+ on_read_ready => sub { },
+ on_write_ready => sub { },
+ );
+
+ identical( $handle->read_handle, $handle->write_handle,
+ '->new with equal read and write fileno only creates one handle' );
+}
+
+done_testing;
+
+package TestHandle;
+use base qw( IO::Async::Handle );
+
+sub on_read_ready { $sub_readready = 1 }
+sub on_write_ready { $sub_writeready = 1 }
diff --git a/t/21stream-1read.t b/t/21stream-1read.t
new file mode 100644
index 0000000..aa49453
--- /dev/null
+++ b/t/21stream-1read.t
@@ -0,0 +1,637 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+use Test::Fatal;
+use Test::Refcount;
+
+use IO::File;
+use POSIX qw( ECONNRESET );
+
+use IO::Async::Loop;
+
+use IO::Async::OS;
+
+use IO::Async::Stream;
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+sub mkhandles
+{
+ my ( $rd, $wr ) = IO::Async::OS->pipepair or die "Cannot pipe() - $!";
+ # Need handles in nonblocking mode
+ $rd->blocking( 0 );
+ $wr->blocking( 0 );
+
+ return ( $rd, $wr );
+}
+
+{
+ my ( $rd, $wr ) = mkhandles;
+
+ my @lines;
+
+ my $stream = IO::Async::Stream->new(
+ read_handle => $rd,
+ on_read => sub {
+ my $self = shift;
+ my ( $buffref, $eof ) = @_;
+
+ push @lines, $1 while $$buffref =~ s/^(.*\n)//;
+ return 0;
+ },
+ );
+
+ ok( defined $stream, 'reading $stream defined' );
+ isa_ok( $stream, "IO::Async::Stream", 'reading $stream isa IO::Async::Stream' );
+
+ is_oneref( $stream, 'reading $stream has refcount 1 initially' );
+
+ $loop->add( $stream );
+
+ is_refcount( $stream, 2, 'reading $stream has refcount 2 after adding to Loop' );
+
+ $wr->syswrite( "message\n" );
+
+ is_deeply( \@lines, [], '@lines before wait' );
+
+ wait_for { scalar @lines };
+
+ is_deeply( \@lines, [ "message\n" ], '@lines after wait' );
+
+ undef @lines;
+
+ $wr->syswrite( "return" );
+
+ $loop->loop_once( 0.1 ); # nothing happens
+
+ is_deeply( \@lines, [], '@lines partial still empty' );
+
+ $wr->syswrite( "\n" );
+
+ wait_for { scalar @lines };
+
+ is_deeply( \@lines, [ "return\n" ], '@lines partial completed now received' );
+
+ undef @lines;
+
+ $wr->syswrite( "hello\nworld\n" );
+ wait_for { scalar @lines };
+
+ is_deeply( \@lines, [ "hello\n", "world\n" ], '@lines two at once' );
+
+ undef @lines;
+ my @new_lines;
+ $stream->configure(
+ on_read => sub {
+ my $self = shift;
+ my ( $buffref, $eof ) = @_;
+
+ push @new_lines, $1 while $$buffref =~ s/^(.*\n)//;
+ return 0;
+ },
+ );
+
+ $wr->syswrite( "new\nlines\n" );
+
+ wait_for { scalar @new_lines };
+
+ is( scalar @lines, 0, '@lines still empty after on_read replace' );
+ is_deeply( \@new_lines, [ "new\n", "lines\n" ], '@new_lines after on_read replace' );
+
+ is_refcount( $stream, 2, 'reading $stream has refcount 2 before removing from Loop' );
+
+ $loop->remove( $stream );
+
+ is_oneref( $stream, 'reading $stream refcount 1 finally' );
+}
+
+# Abstract reading with reader function
+{
+ my ( $rd, $wr ) = mkhandles;
+ my $buffer = "Here is the contents\n";
+
+ my @lines;
+ my $stream = IO::Async::Stream->new(
+ read_handle => $rd,
+ reader => sub {
+ my $self = shift;
+ my $more = substr( $buffer, 0, $_[2], "" );
+ $_[1] .= $more;
+ return length $more;
+ },
+ on_read => sub {
+ my $self = shift;
+ my ( $buffref, $eof ) = @_;
+
+ push @lines, $1 while $$buffref =~ s/^(.*\n)//;
+ return 0;
+ },
+ );
+
+ $loop->add( $stream );
+
+ # make it readready
+ $wr->syswrite( "1" );
+
+ wait_for { scalar @lines };
+
+ is_deeply( \@lines, [ "Here is the contents\n" ], '@lines from stream with abstract reader' );
+
+ $loop->remove( $stream );
+}
+
+# ->want_readready_for_write
+{
+ my ( $rd, $wr ) = mkhandles;
+
+ my $reader_called;
+ my $writer_called;
+ my $stream = IO::Async::Stream->new(
+ handle => $rd,
+ on_read => sub { return 0; }, # ignore reading
+ reader => sub { $reader_called++; sysread( $rd, $_[2], $_[3] ) },
+ writer => sub { $writer_called++; return 1 },
+ );
+
+ $loop->add( $stream );
+
+ # Hacky hack - make the stream want to write, but don't mark the stream write-ready
+ $stream->write( "A" );
+ $stream->want_writeready_for_write( 0 );
+ # End hack
+
+ # make it readready
+ $wr->syswrite( "1" );
+
+ wait_for { $reader_called };
+
+ ok( !$writer_called, 'writer not yet called before ->want_readready_for_write' );
+
+ $stream->want_readready_for_write( 1 );
+
+ undef $reader_called;
+ $wr->syswrite( "2" );
+ wait_for { $reader_called && $writer_called };
+
+ ok( $writer_called, 'writer now invoked with ->want_readready_for_write' );
+
+ $loop->remove( $stream );
+}
+
+{
+ my ( $rd, $wr ) = mkhandles;
+
+ my @chunks;
+
+ my $stream = IO::Async::Stream->new(
+ read_handle => $rd,
+ read_len => 2,
+ on_read => sub {
+ my ( $self, $buffref, $eof ) = @_;
+ push @chunks, $$buffref;
+ $$buffref = "";
+ },
+ );
+
+ $loop->add( $stream );
+
+ $wr->syswrite( "partial" );
+
+ wait_for { scalar @chunks };
+
+ is_deeply( \@chunks, [ "pa" ], '@lines with read_len=2 without read_all' );
+
+ wait_for { @chunks == 4 };
+
+ is_deeply( \@chunks, [ "pa", "rt", "ia", "l" ], '@lines finally with read_len=2 without read_all' );
+
+ undef @chunks;
+ $stream->configure( read_all => 1 );
+
+ $wr->syswrite( "partial" );
+
+ wait_for { scalar @chunks };
+
+ is_deeply( \@chunks, [ "pa", "rt", "ia", "l" ], '@lines with read_len=2 with read_all' );
+
+ $loop->remove( $stream );
+}
+
+{
+ my ( $rd, $wr ) = mkhandles;
+
+ my $no_on_read_stream;
+ ok( !exception { $no_on_read_stream = IO::Async::Stream->new( read_handle => $rd ) },
+ 'Allowed to construct a Stream without an on_read handler' );
+ ok( exception { $loop->add( $no_on_read_stream ) },
+ 'Not allowed to add an on_read-less Stream to a Loop' );
+}
+
+# Subclass
+my @sub_lines;
+
+{
+ my ( $rd, $wr ) = mkhandles;
+
+ my $stream = TestStream->new(
+ read_handle => $rd,
+ );
+
+ ok( defined $stream, 'reading subclass $stream defined' );
+ isa_ok( $stream, "IO::Async::Stream", 'reading $stream isa IO::Async::Stream' );
+
+ is_oneref( $stream, 'subclass $stream has refcount 1 initially' );
+
+ $loop->add( $stream );
+
+ is_refcount( $stream, 2, 'subclass $stream has refcount 2 after adding to Loop' );
+
+ $wr->syswrite( "message\n" );
+
+ is_deeply( \@sub_lines, [], '@sub_lines before wait' );
+
+ wait_for { scalar @sub_lines };
+
+ is_deeply( \@sub_lines, [ "message\n" ], '@sub_lines after wait' );
+
+ $loop->remove( $stream );
+}
+
+# Dynamic on_read chaining
+{
+ my ( $rd, $wr ) = mkhandles;
+
+ my $outer_count = 0;
+ my $inner_count = 0;
+
+ my $record;
+
+ my $stream = IO::Async::Stream->new(
+ read_handle => $rd,
+ on_read => sub {
+ my ( $self, $buffref, $eof ) = @_;
+ $outer_count++;
+
+ return 0 unless $$buffref =~ s/^(.*\n)//;
+
+ my $length = $1;
+
+ return sub {
+ my ( $self, $buffref, $eof ) = @_;
+ $inner_count++;
+
+ return 0 unless length $$buffref >= $length;
+
+ $record = substr( $$buffref, 0, $length, "" );
+
+ return undef;
+ }
+ },
+ );
+
+ is_oneref( $stream, 'dynamic reading $stream has refcount 1 initially' );
+
+ $loop->add( $stream );
+
+ $wr->syswrite( "11" ); # No linefeed yet
+ wait_for { $outer_count > 0 };
+ is( $outer_count, 1, '$outer_count after idle' );
+ is( $inner_count, 0, '$inner_count after idle' );
+
+ $wr->syswrite( "\n" );
+ wait_for { $inner_count > 0 };
+ is( $outer_count, 2, '$outer_count after received length' );
+ is( $inner_count, 1, '$inner_count after received length' );
+
+ $wr->syswrite( "Hello " );
+ wait_for { $inner_count > 1 };
+ is( $outer_count, 2, '$outer_count after partial body' );
+ is( $inner_count, 2, '$inner_count after partial body' );
+
+ $wr->syswrite( "world" );
+ wait_for { $inner_count > 2 };
+ is( $outer_count, 3, '$outer_count after complete body' );
+ is( $inner_count, 3, '$inner_count after complete body' );
+ is( $record, "Hello world", '$record after complete body' );
+
+ $loop->remove( $stream );
+
+ is_oneref( $stream, 'dynamic reading $stream has refcount 1 finally' );
+}
+
+# ->push_on_read
+{
+ my ( $rd, $wr ) = mkhandles;
+
+ my $base;
+ my $stream = IO::Async::Stream->new( read_handle => $rd,
+ on_read => sub {
+ my ( $self, $buffref ) = @_;
+ $base = $$buffref; $$buffref = "";
+ return 0;
+ },
+ );
+
+ $loop->add( $stream );
+
+ my $firstline;
+ $stream->push_on_read(
+ sub {
+ my ( $stream, $buffref, $eof ) = @_;
+ return 0 unless $$buffref =~ s/(.*)\n//;
+ $firstline = $1;
+ return undef;
+ }
+ );
+
+ my $eightbytes;
+ $stream->push_on_read(
+ sub {
+ my ( $stream, $buffref, $eof ) = @_;
+ return 0 unless length $$buffref >= 8;
+ $eightbytes = substr( $$buffref, 0, 8, "" );
+ return undef;
+ }
+ );
+
+ $wr->syswrite( "The first line\nABCDEFGHIJK" );
+
+ wait_for { defined $firstline and defined $eightbytes };
+
+ is( $firstline, "The first line", '$firstline from ->push_on_read CODE' );
+ is( $eightbytes, "ABCDEFGH", '$eightbytes from ->push_on_read CODE' );
+ is( $base, "IJK", '$base from ->push_on_read CODE' );
+
+ $loop->remove( $stream );
+}
+
+# EOF
+{
+ my ( $rd, $wr ) = mkhandles;
+
+ my $eof = 0;
+ my $partial;
+
+ my $stream = IO::Async::Stream->new( read_handle => $rd,
+ on_read => sub {
+ my ( undef, $buffref, $eof ) = @_;
+ $partial = $$buffref if $eof;
+ return 0;
+ },
+ on_read_eof => sub { $eof++ },
+ );
+
+ $loop->add( $stream );
+
+ $wr->syswrite( "Incomplete" );
+
+ $wr->close;
+
+ ok( !$stream->is_read_eof, '$stream ->is_read_eof before wait' );
+ is( $eof, 0, 'EOF indication before wait' );
+
+ wait_for { $eof };
+
+ ok( $stream->is_read_eof, '$stream ->is_read_eof after wait' );
+ is( $eof, 1, 'EOF indication after wait' );
+ is( $partial, "Incomplete", 'EOF stream retains partial input' );
+
+ ok( !defined $stream->loop, 'EOF stream no longer member of Loop' );
+ ok( !defined $stream->read_handle, 'Stream no longer has a read_handle' );
+}
+
+# Disabled close_on_read_eof
+{
+ my ( $rd, $wr ) = mkhandles;
+
+ my $eof = 0;
+ my $partial;
+
+ my $stream = IO::Async::Stream->new( read_handle => $rd,
+ on_read => sub {
+ my ( undef, $buffref, $eof ) = @_;
+ $partial = $$buffref if $eof;
+ return 0;
+ },
+ on_read_eof => sub { $eof++ },
+ close_on_read_eof => 0,
+ );
+
+ $loop->add( $stream );
+
+ $wr->syswrite( "Incomplete" );
+
+ $wr->close;
+
+ is( $eof, 0, 'EOF indication before wait' );
+
+ wait_for { $eof };
+
+ is( $eof, 1, 'EOF indication after wait' );
+ is( $partial, "Incomplete", 'EOF stream retains partial input' );
+
+ ok( defined $stream->loop, 'EOF stream still member of Loop' );
+ ok( defined $stream->read_handle, 'Stream still has a read_handle' );
+}
+
+# Close
+{
+ my ( $rd, $wr ) = mkhandles;
+
+ my $closed = 0;
+ my $loop_during_closed;
+
+ my $stream = IO::Async::Stream->new( read_handle => $rd,
+ on_read => sub { },
+ on_closed => sub {
+ my ( $self ) = @_;
+ $closed = 1;
+ $loop_during_closed = $self->loop;
+ },
+ );
+
+ is_oneref( $stream, 'closing $stream has refcount 1 initially' );
+
+ $loop->add( $stream );
+
+ is_refcount( $stream, 2, 'closing $stream has refcount 2 after adding to Loop' );
+
+ is( $closed, 0, 'closed before close' );
+
+ $stream->close;
+
+ is( $closed, 1, 'closed after close' );
+ is( $loop_during_closed, $loop, 'loop during closed' );
+
+ ok( !defined $stream->loop, 'Stream no longer member of Loop' );
+
+ is_oneref( $stream, 'closing $stream refcount 1 finally' );
+}
+
+# ->read Futures
+{
+ my ( $rd, $wr ) = mkhandles;
+
+ my $stream = IO::Async::Stream->new( read_handle => $rd,
+ on_read => sub {
+ my ( $self, $buffref ) = @_;
+ die "Base on_read invoked with data in the buffer" if length $$buffref;
+ return 0;
+ },
+ );
+
+ $loop->add( $stream );
+
+ my $f_atmost = $stream->read_atmost( 256 );
+
+ $wr->syswrite( "Some data\n" );
+ wait_for { $f_atmost->is_ready };
+
+ is( scalar $f_atmost->get, "Some data\n", '->read_atmost' );
+
+ my $f_exactly = $stream->read_exactly( 4 );
+ my $f_until_qr = $stream->read_until( qr/[A-Z][a-z]*/ );
+ my $f_until_str = $stream->read_until( "\n" );
+
+ $wr->syswrite( "Here is the First line of input\n" );
+
+ wait_for { $f_exactly->is_ready and $f_until_qr->is_ready and $f_until_str->is_ready };
+
+ is( scalar $f_exactly->get, "Here", '->read_exactly' );
+ is( scalar $f_until_qr->get, " is the First", '->read_until regexp' );
+ is( scalar $f_until_str->get, " line of input\n", '->read_until str' );
+
+ my $f_first = $stream->read_until( "\n" );
+ my $f_second = $stream->read_until( "\n" );
+ $f_first->cancel;
+
+ $wr->syswrite( "For the second\n" );
+
+ wait_for { $f_second->is_ready };
+
+ is( scalar $f_second->get, "For the second\n", 'Second ->read_until recieves data after first is ->cancelled' );
+
+ my $f_until_eof = $stream->read_until_eof;
+
+ $wr->syswrite( "And the rest of it" );
+ $wr->close;
+
+ wait_for { $f_until_eof->is_ready };
+
+ is( scalar $f_until_eof->get, "And the rest of it", '->read_until_eof' );
+
+ # No need to remove as ->close did it
+}
+
+# RT101774
+{
+ my ( $rd, $wr ) = mkhandles;
+
+ my $stream = IO::Async::Stream->new( read_handle => $rd,
+ on_read => sub { 0 },
+ );
+
+ $loop->add( $stream );
+
+ $wr->syswrite( "lalaLALA" );
+
+ my $f = $stream->read_exactly( 4 )->then( sub {
+ $stream->read_exactly( 4 );
+ });
+
+ wait_for { $f->is_ready };
+
+ is( scalar $f->get, "LALA", 'chained ->read_exactly' );
+
+ $loop->remove( $stream );
+}
+
+# watermarks
+{
+ my ( $rd, $wr ) = mkhandles;
+
+ my $high_hit = 0;
+ my $low_hit = 0;
+
+ my $stream = IO::Async::Stream->new(
+ read_handle => $rd,
+ on_read => sub { 0 }, # we'll work by Futures
+ read_high_watermark => 8,
+ read_low_watermark => 4,
+ on_read_high_watermark => sub { $high_hit++ },
+ on_read_low_watermark => sub { $low_hit++ },
+ );
+
+ $loop->add( $stream );
+
+ $wr->syswrite( "1234567890" );
+
+ wait_for { $high_hit };
+ ok( 1, "Reading too much hits high watermark" );
+
+ is( $stream->read_exactly( 8 )->get, "12345678", 'Stream->read_exactly yields bytes' );
+
+ is( $low_hit, 1, 'Low watermark hit after ->read' );
+}
+
+# Errors
+{
+ my ( $rd, $wr ) = mkhandles;
+ $wr->syswrite( "X" ); # ensuring $rd is read-ready
+
+ no warnings 'redefine';
+ local *IO::Handle::sysread = sub {
+ $! = ECONNRESET;
+ return undef;
+ };
+
+ my $read_errno;
+
+ my $stream = IO::Async::Stream->new(
+ read_handle => $rd,
+ on_read => sub {},
+ on_read_error => sub { ( undef, $read_errno ) = @_ },
+ );
+
+ $loop->add( $stream );
+
+ wait_for { defined $read_errno };
+
+ cmp_ok( $read_errno, "==", ECONNRESET, 'errno after failed read' );
+
+ my $f = $stream->read_atmost( 256 );
+
+ wait_for { $f->is_ready };
+ cmp_ok( ( $f->failure )[-1], "==", ECONNRESET, 'failure from ->read_atmost after failed read' );
+
+ $loop->remove( $stream );
+}
+
+{
+ binmode STDIN; # Avoid harmless warning in case -CS is in effect
+ my $stream = IO::Async::Stream->new_for_stdin;
+ is( $stream->read_handle, \*STDIN, 'Stream->new_for_stdin->read_handle is STDIN' );
+}
+
+done_testing;
+
+package TestStream;
+use base qw( IO::Async::Stream );
+
+sub on_read
+{
+ my $self = shift;
+ my ( $buffref, $eof ) = @_;
+
+ return 0 unless $$buffref =~ s/^(.*\n)//;
+
+ push @sub_lines, $1;
+ return 1;
+}
diff --git a/t/21stream-2write.t b/t/21stream-2write.t
new file mode 100644
index 0000000..b49cea8
--- /dev/null
+++ b/t/21stream-2write.t
@@ -0,0 +1,479 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+use Test::Refcount;
+
+use Errno qw( EAGAIN EWOULDBLOCK ECONNRESET );
+
+use IO::Async::Loop;
+
+use IO::Async::OS;
+
+use IO::Async::Stream;
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+sub mkhandles
+{
+ my ( $rd, $wr ) = IO::Async::OS->pipepair or die "Cannot pipe() - $!";
+ # Need handles in nonblocking mode
+ $rd->blocking( 0 );
+ $wr->blocking( 0 );
+
+ return ( $rd, $wr );
+}
+
+# useful test function
+sub read_data
+{
+ my ( $s ) = @_;
+
+ my $buffer;
+ my $ret = $s->sysread( $buffer, 8192 );
+
+ return $buffer if( defined $ret && $ret > 0 );
+ die "Socket closed" if( defined $ret && $ret == 0 );
+ return "" if $! == EAGAIN or $! == EWOULDBLOCK;
+ die "Cannot sysread() - $!";
+}
+
+{
+ my ( $rd, $wr ) = mkhandles;
+
+ my $empty;
+
+ my $stream = IO::Async::Stream->new(
+ write_handle => $wr,
+ on_outgoing_empty => sub { $empty = 1 },
+ );
+
+ ok( defined $stream, 'writing $stream defined' );
+ isa_ok( $stream, "IO::Async::Stream", 'writing $stream isa IO::Async::Stream' );
+
+ is_oneref( $stream, 'writing $stream has refcount 1 initially' );
+
+ $loop->add( $stream );
+
+ is_refcount( $stream, 2, 'writing $stream has refcount 2 after adding to Loop' );
+
+ ok( !$stream->want_writeready, 'want_writeready before write' );
+ $stream->write( "message\n" );
+
+ ok( $stream->want_writeready, 'want_writeready after write' );
+
+ wait_for { $empty };
+
+ ok( !$stream->want_writeready, 'want_writeready after wait' );
+ is( $empty, 1, '$empty after writing buffer' );
+
+ is( read_data( $rd ), "message\n", 'data after writing buffer' );
+
+ my $written = 0;
+ my $flushed;
+
+ my $f = $stream->write( "hello again\n",
+ on_write => sub {
+ is( $_[0], $stream, 'on_write $_[0] is $stream' );
+ $written += $_[1];
+ },
+ on_flush => sub {
+ is( $_[0], $stream, 'on_flush $_[0] is $stream' );
+ $flushed++
+ },
+ );
+
+ ok( !$f->is_ready, '->write future not yet ready' );
+
+ wait_for { $flushed };
+
+ ok( $f->is_ready, '->write future is ready after flush' );
+ is( $written, 12, 'on_write given total write length after flush' );
+ is( read_data( $rd ), "hello again\n", 'flushed data does get flushed' );
+
+ $flushed = 0;
+ $stream->write( "", on_flush => sub { $flushed++ } );
+ wait_for { $flushed };
+
+ ok( 1, "write empty data with on_flush" );
+
+ $stream->configure( autoflush => 1 );
+ $stream->write( "immediate\n" );
+
+ ok( !$stream->want_writeready, 'not want_writeready after autoflush write' );
+ is( read_data( $rd ), "immediate\n", 'data after autoflush write' );
+
+ $stream->configure( autoflush => 0 );
+ $stream->write( "partial " );
+ $stream->configure( autoflush => 1 );
+ $stream->write( "data\n" );
+
+ ok( !$stream->want_writeready, 'not want_writeready after split autoflush write' );
+ is( read_data( $rd ), "partial data\n", 'data after split autoflush write' );
+
+ is_refcount( $stream, 2, 'writing $stream has refcount 2 before removing from Loop' );
+
+ $loop->remove( $stream );
+
+ is_oneref( $stream, 'writing $stream refcount 1 finally' );
+}
+
+# Abstract writing with writer function
+{
+ my ( $rd, $wr ) = mkhandles;
+ my $buffer;
+
+ my $stream = IO::Async::Stream->new(
+ write_handle => $wr,
+ writer => sub {
+ my $self = shift;
+ $buffer .= substr( $_[1], 0, $_[2], "" );
+ return $_[2];
+ },
+ );
+
+ $loop->add( $stream );
+
+ my $flushed;
+ $stream->write( "Some data for abstract buffer\n", on_flush => sub { $flushed++ } );
+
+ wait_for { $flushed };
+
+ is( $buffer, "Some data for abstract buffer\n", '$buffer after ->write to stream with abstract writer' );
+
+ $loop->remove( $stream );
+}
+
+# ->want_writeready_for_read
+{
+ my ( $rd, $wr ) = mkhandles;
+
+ my $reader_called;
+ my $stream = IO::Async::Stream->new(
+ handle => $wr,
+ on_read => sub { return 0; }, # ignore reading
+ reader => sub { $reader_called++; $! = EAGAIN; return undef },
+ );
+
+ $loop->add( $stream );
+
+ $loop->loop_once( 0.1 ); # haaaaack
+
+ ok( !$reader_called, 'reader not yet called before ->want_writeready_for_read' );
+
+ $stream->want_writeready_for_read( 1 );
+
+ wait_for { $reader_called };
+
+ ok( $reader_called, 'reader now invoked with ->want_writeready_for_read' );
+
+ $loop->remove( $stream );
+}
+
+# on_writeable_{start,stop}
+{
+ my ( $rd, $wr ) = mkhandles;
+ my $buffer;
+
+ my $writeable;
+ my $unwriteable;
+ my $emulate_writeable = 0;
+ my $stream = IO::Async::Stream->new(
+ write_handle => $wr,
+ writer => sub {
+ my $self = shift;
+ $! = EAGAIN, return undef unless $emulate_writeable;
+
+ $buffer .= substr( $_[1], 0, $_[2], "" );
+ return $_[2];
+ },
+ on_writeable_start => sub { $writeable++ },
+ on_writeable_stop => sub { $unwriteable++ },
+ );
+
+ $loop->add( $stream );
+
+ $stream->write( "Something" );
+
+ wait_for { $unwriteable };
+
+ $emulate_writeable = 1;
+
+ wait_for { $writeable };
+
+ is( $buffer, "Something", '$buffer after emulated EAGAIN' );
+
+ $loop->remove( $stream );
+}
+
+{
+ my ( $rd, $wr ) = mkhandles;
+
+ my $stream = IO::Async::Stream->new(
+ write_handle => $wr,
+ write_len => 2,
+ );
+
+ $loop->add( $stream );
+
+ $stream->write( "partial" );
+
+ $loop->loop_once( 0.1 );
+
+ is( read_data( $rd ), "pa", 'data after writing buffer with write_len=2 without write_all');
+
+ $loop->loop_once( 0.1 ) for 1 .. 3;
+
+ is( read_data( $rd ), "rtial", 'data finally after writing buffer with write_len=2 without write_all' );
+
+ $stream->configure( write_all => 1 );
+
+ $stream->write( "partial" );
+
+ $loop->loop_once( 0.1 );
+
+ is( read_data( $rd ), "partial", 'data after writing buffer with write_len=2 with write_all');
+
+ $loop->remove( $stream );
+}
+
+# EOF
+SKIP: {
+ skip "This loop cannot detect hangup condition", 5 unless $loop->_CAN_ON_HANGUP;
+
+ my ( $rd, $wr ) = mkhandles;
+
+ local $SIG{PIPE} = "IGNORE";
+
+ my $eof = 0;
+
+ my $stream = IO::Async::Stream->new( write_handle => $wr,
+ on_write_eof => sub { $eof++ },
+ );
+
+ $loop->add( $stream );
+
+ my $write_future = $stream->write( "Junk" );
+
+ $rd->close;
+
+ ok( !$stream->is_write_eof, '$stream->is_write_eof before wait' );
+ is( $eof, 0, 'EOF indication before wait' );
+
+ wait_for { $eof };
+
+ ok( $stream->is_write_eof, '$stream->is_write_eof after wait' );
+ is( $eof, 1, 'EOF indication after wait' );
+
+ ok( !defined $stream->loop, 'EOF stream no longer member of Loop' );
+
+ ok( $write_future->is_ready,'write future ready after stream closed' );
+ ok( $write_future->is_failed,'write future failed after stream closed' );
+}
+
+# Close
+{
+ my ( $rd, $wr ) = mkhandles;
+
+ my $closed = 0;
+ my $loop_during_closed;
+
+ my $stream = IO::Async::Stream->new( write_handle => $wr,
+ on_closed => sub {
+ my ( $self ) = @_;
+ $closed = 1;
+ $loop_during_closed = $self->loop;
+ },
+ );
+
+ is_oneref( $stream, 'closing $stream has refcount 1 initially' );
+
+ $stream->write( "hello" );
+
+ $loop->add( $stream );
+
+ is_refcount( $stream, 2, 'closing $stream has refcount 2 after adding to Loop' );
+
+ is( $closed, 0, 'closed before close' );
+
+ $stream->close_when_empty;
+
+ is( $closed, 0, 'closed after close' );
+
+ wait_for { $closed };
+
+ is( $closed, 1, 'closed after wait' );
+ is( $loop_during_closed, $loop, 'loop during closed' );
+
+ ok( !defined $stream->loop, 'Stream no longer member of Loop' );
+
+ is_oneref( $stream, 'closing $stream refcount 1 finally' );
+}
+
+# ->write( Future )
+{
+ my ( $rd, $wr ) = mkhandles;
+ my $stream = IO::Async::Stream->new(
+ write_handle => $wr,
+ );
+ $loop->add( $stream );
+
+ my $written = 0;
+ my $flushed;
+ $stream->write(
+ my $future = $loop->new_future,
+ on_write => sub { $written += $_[1] },
+ on_flush => sub { $flushed++ },
+ );
+
+ $loop->loop_once( 0.1 );
+ is( read_data( $rd ), "", 'stream idle before Future completes' );
+
+ $future->done( "some data to write" );
+
+ wait_for { $flushed };
+
+ is( $written, 18, 'stream written by Future completion invokes on_write' );
+
+ is( read_data( $rd ), "some data to write", 'stream written by Future completion' );
+
+ $loop->remove( $stream );
+}
+
+# ->write( CODE )
+{
+ my ( $rd, $wr ) = mkhandles;
+ my $stream = IO::Async::Stream->new(
+ write_handle => $wr,
+ );
+ $loop->add( $stream );
+
+ my $done;
+ my $written = 0;
+ my $flushed;
+
+ $stream->write(
+ sub {
+ is( $_[0], $stream, 'Writersub $_[0] is $stream' );
+ return $done++ ? undef : "a lazy message\n";
+ },
+ on_write => sub { $written += $_[1] },
+ on_flush => sub { $flushed++ },
+ );
+
+ $flushed = 0;
+ wait_for { $flushed };
+
+ is( $written, 15, 'stream written by generator CODE invokes on_write' );
+
+ is( read_data( $rd ), "a lazy message\n", 'lazy data was written' );
+
+ my @chunks = ( "some ", "message chunks ", "here\n" );
+
+ $stream->write(
+ sub {
+ return shift @chunks;
+ },
+ on_flush => sub { $flushed++ },
+ );
+
+ $flushed = 0;
+ wait_for { $flushed };
+
+ is( read_data( $rd ), "some message chunks here\n", 'multiple lazy data was written' );
+
+ $loop->remove( $stream );
+}
+
+# ->write mixed returns
+{
+ my ( $rd, $wr ) = mkhandles;
+ my $stream = IO::Async::Stream->new(
+ write_handle => $wr,
+ );
+ $loop->add( $stream );
+
+ my $flushed;
+ $stream->write( my $future = $loop->new_future, on_flush => sub { $flushed++ } );
+
+ my $once = 0;
+ $future->done( sub {
+ return $once++ ? undef : ( $future = $loop->new_future );
+ });
+
+ wait_for { $once };
+
+ $future->done( "Eventual string" );
+
+ wait_for { $flushed };
+
+ is( read_data( $rd ), "Eventual string", 'multiple lazy data was written' );
+
+ $loop->remove( $stream );
+}
+
+{
+ my ( $rd, $wr ) = mkhandles;
+
+ my $stream = IO::Async::Stream->new;
+
+ my $flushed;
+
+ $stream->write( "Prequeued data", on_flush => sub { $flushed++ } );
+
+ $stream->configure( write_handle => $wr );
+
+ $loop->add( $stream );
+
+ wait_for { $flushed };
+
+ ok( 1, 'prequeued data gets flushed' );
+
+ is( read_data( $rd ), "Prequeued data", 'prequeued data gets written' );
+
+ $loop->remove( $stream );
+}
+
+# Errors
+{
+ my ( $rd, $wr ) = mkhandles;
+
+ no warnings 'redefine';
+ local *IO::Handle::syswrite = sub {
+ $! = ECONNRESET;
+ return undef;
+ };
+
+ my $write_errno;
+
+ my $stream = IO::Async::Stream->new(
+ write_handle => $wr,
+ on_write_error => sub { ( undef, $write_errno ) = @_ },
+ );
+
+ $loop->add( $stream );
+
+ my $write_future = $stream->write( "hello" );
+
+ wait_for { defined $write_errno };
+
+ cmp_ok( $write_errno, "==", ECONNRESET, 'errno after failed write' );
+
+ ok( $write_future->is_ready,'write future ready after failed write' );
+ ok( $write_future->is_failed,'write future failed after failed write' );
+
+ $loop->remove( $stream );
+}
+
+{
+ my $stream = IO::Async::Stream->new_for_stdout;
+ is( $stream->write_handle, \*STDOUT, 'Stream->new_for_stdout->write_handle is STDOUT' );
+}
+
+done_testing;
diff --git a/t/21stream-3split.t b/t/21stream-3split.t
new file mode 100644
index 0000000..1fd99e4
--- /dev/null
+++ b/t/21stream-3split.t
@@ -0,0 +1,187 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+use Test::Fatal;
+use Test::Refcount;
+
+use IO::File;
+use Errno qw( EAGAIN EWOULDBLOCK );
+
+use IO::Async::Loop;
+
+use IO::Async::OS;
+
+use IO::Async::Stream;
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+my ( $S1, $S2 ) = IO::Async::OS->socketpair or die "Cannot create socket pair - $!";
+my ( $S3, $S4 ) = IO::Async::OS->socketpair or die "Cannot create socket pair - $!";
+
+# Need sockets in nonblocking mode
+$_->blocking( 0 ) for $S1, $S2, $S3, $S4;
+
+# useful test function
+sub read_data
+{
+ my ( $s ) = @_;
+
+ my $buffer;
+ my $ret = $s->sysread( $buffer, 8192 );
+
+ return $buffer if( defined $ret && $ret > 0 );
+ die "Socket closed" if( defined $ret && $ret == 0 );
+ return "" if $! == EAGAIN or $! == EWOULDBLOCK;
+ die "Cannot sysread() - $!";
+}
+
+my @lines;
+
+my $stream = IO::Async::Stream->new(
+ read_handle => $S2,
+ write_handle => $S3,
+ on_read => sub {
+ my $self = shift;
+ my ( $buffref, $eof ) = @_;
+
+ push @lines, $1 while $$buffref =~ s/^(.*\n)//;
+ return 0;
+ },
+);
+
+is_oneref( $stream, 'split read/write $stream has refcount 1 initially' );
+
+undef @lines;
+
+$loop->add( $stream );
+
+is_refcount( $stream, 2, 'split read/write $stream has refcount 2 after adding to Loop' );
+
+$stream->write( "message\n" );
+
+$loop->loop_once( 0.1 );
+
+is( read_data( $S4 ), "message\n", '$S4 receives data from split stream' );
+is( read_data( $S1 ), "", '$S1 empty from split stream' );
+
+$S1->syswrite( "reverse\n" );
+
+$loop->loop_once( 0.1 );
+
+is_deeply( \@lines, [ "reverse\n" ], '@lines on response to split stream' );
+
+is_refcount( $stream, 2, 'split read/write $stream has refcount 2 before removing from Loop' );
+
+$loop->remove( $stream );
+
+is_oneref( $stream, 'split read/write $stream refcount 1 finally' );
+
+undef $stream;
+
+my $buffer = "";
+my $closed;
+
+$stream = IO::Async::Stream->new(
+ # No handle yet
+ on_read => sub {
+ my ( $self, $buffref, $eof ) = @_;
+ $buffer .= $$buffref;
+ $$buffref = "";
+ return 0;
+ },
+ on_closed => sub {
+ my ( $self ) = @_;
+ $closed = 1;
+ },
+);
+
+is_oneref( $stream, 'latehandle $stream has refcount 1 initially' );
+
+$loop->add( $stream );
+
+is_refcount( $stream, 2, 'latehandle $stream has refcount 2 after adding to Loop' );
+
+ok( exception { $stream->write( "some text" ) },
+ '->write on stream with no IO handle fails' );
+
+$stream->set_handle( $S1 );
+
+is_refcount( $stream, 2, 'latehandle $stream has refcount 2 after setting a handle' );
+
+$stream->write( "some text" );
+
+$loop->loop_once( 0.1 );
+
+my $buffer2;
+$S2->sysread( $buffer2, 8192 );
+
+is( $buffer2, "some text", 'stream-written text appears' );
+
+$S2->syswrite( "more text" );
+
+wait_for { length $buffer };
+
+is( $buffer, "more text", 'stream-read text appears' );
+
+$stream->close_when_empty;
+
+is( $closed, 1, 'closed after close' );
+
+ok( !defined $stream->loop, 'Stream no longer member of Loop' );
+
+is_oneref( $stream, 'latehandle $stream refcount 1 finally' );
+
+# Now try re-opening the stream with a new handle, and check it continues to
+# work
+
+$loop->add( $stream );
+
+$stream->set_handle( $S3 );
+
+$stream->write( "more text" );
+
+$loop->loop_once( 0.1 );
+
+undef $buffer2;
+$S4->sysread( $buffer2, 8192 );
+
+is( $buffer2, "more text", 'stream-written text appears after reopen' );
+
+$loop->remove( $stream );
+
+undef $stream;
+
+( $S1, $S2 ) = IO::Async::OS->socketpair or die "Cannot socketpair - $!";
+
+$stream = IO::Async::Stream->new(
+ handle => $S1,
+ on_read => sub { },
+);
+
+$stream->write( "hello" );
+
+$loop->add( $stream );
+
+is_refcount( $stream, 2, '$stream has two references' );
+undef $stream; # Only ref is now in the Loop
+
+$S2->close;
+
+# $S1 should now be both read- and write-ready.
+ok( !exception { $loop->loop_once }, 'read+write-ready closed Stream doesn\'t die' );
+
+undef $stream;
+
+binmode STDIN; # Avoid harmless warning in case -CS is in effect
+$stream = IO::Async::Stream->new_for_stdio;
+is( $stream->read_handle, \*STDIN, 'Stream->new_for_stdio->read_handle is STDIN' );
+is( $stream->write_handle, \*STDOUT, 'Stream->new_for_stdio->write_handle is STDOUT' );
+
+done_testing;
diff --git a/t/21stream-4encoding.t b/t/21stream-4encoding.t
new file mode 100644
index 0000000..cae0cac
--- /dev/null
+++ b/t/21stream-4encoding.t
@@ -0,0 +1,151 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+use Test::Fatal;
+use Test::Refcount;
+
+use Errno qw( EAGAIN EWOULDBLOCK );
+
+use IO::Async::Loop;
+
+use IO::Async::OS;
+
+use IO::Async::Stream;
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+sub mkhandles
+{
+ my ( $rd, $wr ) = IO::Async::OS->pipepair or die "Cannot pipe() - $!";
+ # Need handles in nonblocking mode
+ $rd->blocking( 0 );
+ $wr->blocking( 0 );
+
+ return ( $rd, $wr );
+}
+
+# useful test function
+sub read_data
+{
+ my ( $s ) = @_;
+
+ my $buffer;
+ my $ret = $s->sysread( $buffer, 8192 );
+
+ return $buffer if( defined $ret && $ret > 0 );
+ die "Socket closed" if( defined $ret && $ret == 0 );
+ return "" if $! == EAGAIN or $! == EWOULDBLOCK;
+ die "Cannot sysread() - $!";
+}
+
+# To test correct multi-byte encoding handling, we'll use a UTF-8 character
+# that requires multiple bytes. Furthermore we'll use one that doesn't appear
+# in Latin-1
+#
+# 'ĉ' [U+0109] - LATIN SMALL LETTER C WITH CIRCUMFLEX
+# :0xc4 0x89
+
+# Read encoding
+{
+ my ( $rd, $wr ) = mkhandles;
+
+ my $read = "";
+ my $stream = IO::Async::Stream->new(
+ read_handle => $rd,
+ encoding => "UTF-8",
+ on_read => sub {
+ $read = ${$_[1]};
+ ${$_[1]} = "";
+ return 0;
+ },
+ );
+
+ $loop->add( $stream );
+
+ $wr->syswrite( "\xc4\x89" );
+
+ wait_for { length $read };
+
+ is( $read, "\x{109}", 'Unicode characters read by on_read' );
+
+ $wr->syswrite( "\xc4\x8a\xc4" );
+
+ $read = "";
+ wait_for { length $read };
+
+ is( $read, "\x{10a}", 'Partial UTF-8 character not yet visible' );
+
+ $wr->syswrite( "\x8b" );
+
+ $read = "";
+ wait_for { length $read };
+
+ is( $read, "\x{10b}", 'Partial UTF-8 character visible after completion' );
+
+ # An invalid sequence
+ $wr->syswrite( "\xc4!" );
+
+ $read = "";
+ wait_for { length $read };
+
+ is( $read, "\x{fffd}!", 'Invalid UTF-8 byte yields U+FFFD' );
+
+ $loop->remove( $stream );
+}
+
+# Write encoding
+{
+ my ( $rd, $wr ) = mkhandles;
+
+ my $stream = IO::Async::Stream->new(
+ write_handle => $wr,
+ encoding => "UTF-8",
+ );
+
+ $loop->add( $stream );
+
+ my $flushed;
+ $stream->write( "\x{109}", on_flush => sub { $flushed++ } );
+
+ wait_for { $flushed };
+
+ is( read_data( $rd ), "\xc4\x89", 'UTF-8 bytes written by ->write string' );
+
+ $stream->configure( write_len => 1 );
+
+ $stream->write( "\x{109}" );
+
+ my $byte;
+
+ $loop->loop_once while !length( $byte = read_data( $rd ) );
+ is( $byte, "\xc4", 'First UTF-8 byte written with write_len 1' );
+
+ $loop->loop_once while !length( $byte = read_data( $rd ) );
+ is( $byte, "\x89", 'Remaining UTF-8 byte written with write_len 1' );
+
+ $flushed = 0;
+ $stream->write( Future->done( "\x{10a}" ), on_flush => sub { $flushed++ } );
+
+ wait_for { $flushed };
+
+ is( read_data( $rd ), "\xc4\x8a", 'UTF-8 bytes written by ->write Future' );
+
+ $flushed = 0;
+ my $once = 0;
+ $stream->write( sub { $once++ ? undef : "\x{10b}" }, on_flush => sub { $flushed++ } );
+
+ wait_for { $flushed };
+
+ is( read_data( $rd ), "\xc4\x8b", 'UTF-8 bytes written by ->write CODE' );
+
+ $loop->remove( $stream );
+}
+
+done_testing;
diff --git a/t/22timer-absolute.t b/t/22timer-absolute.t
new file mode 100644
index 0000000..6192bec
--- /dev/null
+++ b/t/22timer-absolute.t
@@ -0,0 +1,143 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+use Test::Refcount;
+use t::TimeAbout;
+
+use Time::HiRes qw( time );
+
+use IO::Async::Timer::Absolute;
+
+use IO::Async::Loop;
+
+use constant AUT => $ENV{TEST_QUICK_TIMERS} ? 0.1 : 1;
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+{
+ my $expired;
+ my @eargs;
+
+ my $timer = IO::Async::Timer::Absolute->new(
+ time => time + 2 * AUT,
+
+ on_expire => sub { @eargs = @_; $expired = 1 },
+ );
+
+ ok( defined $timer, '$timer defined' );
+ isa_ok( $timer, "IO::Async::Timer", '$timer isa IO::Async::Timer' );
+
+ is_oneref( $timer, '$timer has refcount 1 initially' );
+
+ $loop->add( $timer );
+
+ is_refcount( $timer, 2, '$timer has refcount 2 after adding to Loop' );
+
+ ok( $timer->is_running, 'Started Timer is running' );
+
+ time_about( sub { wait_for { $expired } }, 2, 'Timer works' );
+ is_deeply( \@eargs, [ $timer ], 'on_expire args' );
+
+ ok( !$timer->is_running, 'Expired Timer is no longer running' );
+
+ undef @eargs;
+
+ is_refcount( $timer, 2, '$timer has refcount 2 before removing from Loop' );
+
+ $loop->remove( $timer );
+
+ is_oneref( $timer, '$timer has refcount 1 after removing from Loop' );
+}
+
+{
+ my $expired;
+
+ my $timer = IO::Async::Timer::Absolute->new(
+ time => time + 2 * AUT,
+ on_expire => sub { $expired++ },
+ );
+
+ $loop->add( $timer );
+ $loop->remove( $timer );
+
+ $loop->loop_once( 3 * AUT );
+
+ ok( !$expired, "Removed Timer does not expire" );
+}
+
+{
+ my $expired;
+
+ my $timer = IO::Async::Timer::Absolute->new(
+ time => time + 5 * AUT,
+ on_expire => sub { $expired++ },
+ );
+
+ $loop->add( $timer );
+
+ $timer->configure( time => time + 1 * AUT );
+
+ time_about( sub { wait_for { $expired } }, 1, 'Reconfigured timer works' );
+
+ $loop->remove( $timer );
+}
+
+{
+ my $timer = IO::Async::Timer::Absolute->new(
+ time => time + 1 * AUT,
+ on_expire => sub { die "Test failed to replace expiry handler" },
+ );
+
+ $loop->add( $timer );
+
+ my $new_expired;
+ $timer->configure( on_expire => sub { $new_expired = 1 } );
+
+ time_about( sub { wait_for { $new_expired } }, 1, 'Reconfigured timer on_expire works' );
+
+ $loop->remove( $timer );
+}
+
+## Subclass
+
+my $sub_expired;
+{
+ my $timer = TestTimer->new(
+ time => time + 2 * AUT,
+ );
+
+ ok( defined $timer, 'subclass $timer defined' );
+ isa_ok( $timer, "IO::Async::Timer", 'subclass $timer isa IO::Async::Timer' );
+
+ is_oneref( $timer, 'subclass $timer has refcount 1 initially' );
+
+ $loop->add( $timer );
+
+ is_refcount( $timer, 2, 'subclass $timer has refcount 2 after adding to Loop' );
+
+ ok( $timer->is_running, 'Started subclass Timer is running' );
+
+ time_about( sub { wait_for { $sub_expired } }, 2, 'subclass Timer works' );
+
+ ok( !$timer->is_running, 'Expired subclass Timer is no longer running' );
+
+ is_refcount( $timer, 2, 'subclass $timer has refcount 2 before removing from Loop' );
+
+ $loop->remove( $timer );
+
+ is_oneref( $timer, 'subclass $timer has refcount 1 after removing from Loop' );
+}
+
+done_testing;
+
+package TestTimer;
+use base qw( IO::Async::Timer::Absolute );
+
+sub on_expire { $sub_expired = 1 }
diff --git a/t/22timer-countdown.t b/t/22timer-countdown.t
new file mode 100644
index 0000000..db8b49c
--- /dev/null
+++ b/t/22timer-countdown.t
@@ -0,0 +1,257 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+use Test::Fatal;
+use Test::Refcount;
+use t::TimeAbout;
+
+use Time::HiRes qw( time );
+
+use IO::Async::Timer::Countdown;
+
+use IO::Async::Loop;
+
+use constant AUT => $ENV{TEST_QUICK_TIMERS} ? 0.1 : 1;
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+{
+ my $expired;
+ my @eargs;
+
+ my $timer = IO::Async::Timer::Countdown->new(
+ delay => 2 * AUT,
+
+ on_expire => sub { @eargs = @_; $expired = 1 },
+ );
+
+ ok( defined $timer, '$timer defined' );
+ isa_ok( $timer, "IO::Async::Timer", '$timer isa IO::Async::Timer' );
+
+ is_oneref( $timer, '$timer has refcount 1 initially' );
+
+ $loop->add( $timer );
+
+ is_refcount( $timer, 2, '$timer has refcount 2 after adding to Loop' );
+
+ ok( !$timer->is_running, 'New Timer is no yet running' );
+ ok( !$timer->is_expired, 'New Timer is no yet expired' );
+
+ is( $timer->start, $timer, '$timer->start returns $timer' );
+
+ is_refcount( $timer, 2, '$timer has refcount 2 after starting' );
+
+ ok( $timer->is_running, 'Started Timer is running' );
+ ok( !$timer->is_expired, 'Started Timer not yet expired' );
+
+ time_about( sub { wait_for { $expired } }, 2, 'Timer works' );
+ is_deeply( \@eargs, [ $timer ], 'on_expire args' );
+
+ ok( !$timer->is_running, 'Expired Timer is no longer running' );
+ ok( $timer->is_expired, 'Expired Timer now expired' );
+
+ undef @eargs;
+
+ is_refcount( $timer, 2, '$timer has refcount 2 before removing from Loop' );
+
+ $loop->remove( $timer );
+
+ is_oneref( $timer, '$timer has refcount 1 after removing from Loop' );
+
+ undef $expired;
+
+ is( $timer->start, $timer, '$timer->start out of a Loop returns $timer' );
+
+ $loop->add( $timer );
+
+ ok( $timer->is_running, 'Re-started Timer is running' );
+ ok( !$timer->is_expired, 'Re-started Timer not yet expired' );
+
+ time_about( sub { wait_for { $expired } }, 2, 'Timer works a second time' );
+
+ ok( !$timer->is_running, '2nd-time expired Timer is no longer running' );
+ ok( $timer->is_expired, '2nd-time expired Timer now expired' );
+
+ undef $expired;
+ $timer->start;
+
+ $loop->loop_once( 1 * AUT );
+
+ $timer->stop;
+
+ $timer->stop;
+
+ ok( 1, "Timer can be stopped a second time" );
+
+ $loop->loop_once( 2 * AUT );
+
+ ok( !$expired, "Stopped timer doesn't expire" );
+
+ undef $expired;
+ $timer->start;
+
+ $loop->loop_once( 1 * AUT );
+
+ my $now = time;
+ $timer->reset;
+
+ $loop->loop_once( 1.5 * AUT );
+
+ ok( !$expired, "Reset Timer hasn't expired yet" );
+
+ wait_for { $expired };
+ my $took = (time - $now) / AUT;
+
+ cmp_ok( $took, '>', 1.5, "Timer has now expired took at least 1.5" );
+ cmp_ok( $took, '<', 2.5, "Timer has now expired took no more than 2.5" );
+
+ $loop->remove( $timer );
+
+ undef @eargs;
+
+ is_oneref( $timer, 'Timer has refcount 1 finally' );
+}
+
+{
+ my $timer = IO::Async::Timer::Countdown->new(
+ delay => 2 * AUT,
+ on_expire => sub { },
+ );
+
+ $loop->add( $timer );
+
+ $timer->start;
+
+ $loop->remove( $timer );
+
+ $loop->loop_once( 3 * AUT );
+
+ ok( !$timer->is_expired, "Removed Timer does not expire" );
+}
+
+{
+ my $timer = IO::Async::Timer::Countdown->new(
+ delay => 2 * AUT,
+ on_expire => sub { },
+ );
+
+ $timer->start;
+
+ $loop->add( $timer );
+
+ ok( $timer->is_running, 'Pre-started Timer is running after adding' );
+
+ time_about( sub { wait_for { $timer->is_expired } }, 2, 'Pre-started Timer works' );
+
+ $loop->remove( $timer );
+}
+
+{
+ my $timer = IO::Async::Timer::Countdown->new(
+ delay => 2 * AUT,
+ on_expire => sub { },
+ );
+
+ $timer->start;
+ $timer->stop;
+
+ $loop->add( $timer );
+
+ $loop->loop_once( 3 * AUT );
+
+ ok( !$timer->is_expired, "start/stopped Timer doesn't expire" );
+
+ $loop->remove( $timer );
+}
+
+{
+ my $timer = IO::Async::Timer::Countdown->new(
+ delay => 2 * AUT,
+ on_expire => sub { },
+ );
+
+ $loop->add( $timer );
+
+ $timer->configure( delay => 1 * AUT );
+
+ $timer->start;
+
+ time_about( sub { wait_for { $timer->is_expired } }, 1, 'Reconfigured timer delay works' );
+
+ my $expired;
+ $timer->configure( on_expire => sub { $expired = 1 } );
+
+ $timer->start;
+
+ time_about( sub { wait_for { $expired } }, 1, 'Reconfigured timer on_expire works' );
+
+ $timer->start;
+ ok( exception { $timer->configure( delay => 5 ); },
+ 'Configure a running timer fails' );
+
+ $loop->remove( $timer );
+}
+
+{
+ my $timer = IO::Async::Timer::Countdown->new(
+ delay => 1 * AUT,
+ remove_on_expire => 1,
+
+ on_expire => sub { },
+ );
+
+ $loop->add( $timer );
+ $timer->start;
+
+ time_about( sub { wait_for { $timer->is_expired } }, 1, 'remove_on_expire Timer' );
+
+ is( $timer->loop, undef, 'remove_on_expire Timer removed from Loop after expire' );
+}
+
+## Subclass
+
+my $sub_expired;
+{
+ my $timer = TestTimer->new(
+ delay => 2 * AUT,
+ );
+
+ ok( defined $timer, 'subclass $timer defined' );
+ isa_ok( $timer, "IO::Async::Timer", 'subclass $timer isa IO::Async::Timer' );
+
+ is_oneref( $timer, 'subclass $timer has refcount 1 initially' );
+
+ $loop->add( $timer );
+
+ is_refcount( $timer, 2, 'subclass $timer has refcount 2 after adding to Loop' );
+
+ $timer->start;
+
+ is_refcount( $timer, 2, 'subclass $timer has refcount 2 after starting' );
+
+ ok( $timer->is_running, 'Started subclass Timer is running' );
+
+ time_about( sub { wait_for { $sub_expired } }, 2, 'subclass Timer works' );
+
+ ok( !$timer->is_running, 'Expired subclass Timer is no longer running' );
+
+ is_refcount( $timer, 2, 'subclass $timer has refcount 2 before removing from Loop' );
+
+ $loop->remove( $timer );
+
+ is_oneref( $timer, 'subclass $timer has refcount 1 after removing from Loop' );
+}
+
+done_testing;
+
+package TestTimer;
+use base qw( IO::Async::Timer::Countdown );
+
+sub on_expire { $sub_expired = 1 }
diff --git a/t/22timer-periodic.t b/t/22timer-periodic.t
new file mode 100644
index 0000000..a2fc28e
--- /dev/null
+++ b/t/22timer-periodic.t
@@ -0,0 +1,233 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+use Test::Fatal;
+use Test::Refcount;
+use t::TimeAbout;
+
+use IO::Async::Timer::Periodic;
+
+use IO::Async::Loop;
+
+use constant AUT => $ENV{TEST_QUICK_TIMERS} ? 0.1 : 1;
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+{
+ my $tick = 0;
+ my @targs;
+
+ my $timer = IO::Async::Timer::Periodic->new(
+ interval => 2 * AUT,
+
+ on_tick => sub { @targs = @_; $tick++ },
+ );
+
+ ok( defined $timer, '$timer defined' );
+ isa_ok( $timer, "IO::Async::Timer", '$timer isa IO::Async::Timer' );
+
+ is_oneref( $timer, '$timer has refcount 1 initially' );
+
+ $loop->add( $timer );
+
+ is_refcount( $timer, 2, '$timer has refcount 2 after adding to Loop' );
+
+ is( $timer->start, $timer, '$timer->start returns $timer' );
+
+ is_refcount( $timer, 2, '$timer has refcount 2 after starting' );
+
+ ok( $timer->is_running, 'Started Timer is running' );
+
+ time_about( sub { wait_for { $tick == 1 } }, 2, 'Timer works' );
+ is_deeply( \@targs, [ $timer ], 'on_tick args' );
+
+ ok( $timer->is_running, 'Timer is still running' );
+
+ time_about( sub { wait_for { $tick == 2 } }, 2, 'Timer works a second time' );
+
+ $loop->loop_once( 1 * AUT );
+
+ $timer->stop;
+
+ $timer->stop;
+
+ ok( 1, "Timer can be stopped a second time" );
+
+ $loop->loop_once( 2 * AUT );
+
+ ok( $tick == 2, "Stopped timer doesn't tick" );
+
+ undef @targs;
+
+ is_refcount( $timer, 2, '$timer has refcount 2 before removing from Loop' );
+
+ $loop->remove( $timer );
+
+ is_oneref( $timer, '$timer has refcount 1 after removing from Loop' );
+
+ ok( !$timer->is_running, 'Removed timer not running' );
+
+ $loop->add( $timer );
+
+ $timer->configure( interval => 1 * AUT );
+
+ $timer->start;
+
+ time_about( sub { wait_for { $tick == 3 } }, 1, 'Reconfigured timer interval works' );
+
+ $timer->stop;
+
+ $timer->configure( interval => 2 * AUT, first_interval => 0 );
+
+ $timer->start;
+ is( $tick, 3, 'Zero first_interval start not invoked yet' );
+ time_about( sub { wait_for { $tick == 4 } }, 0, 'Zero first_interval invokes callback async' );
+
+ time_about( sub { wait_for { $tick == 5 } }, 2, 'Normal interval used after first invocation' );
+
+ ok( exception { $timer->configure( interval => 5 ); },
+ 'Configure a running timer fails' );
+
+ $loop->remove( $timer );
+
+ undef @targs;
+
+ is_oneref( $timer, 'Timer has refcount 1 finally' );
+}
+
+# reschedule => "skip"
+{
+ my $tick = 0;
+
+ my $timer = IO::Async::Timer::Periodic->new(
+ interval => 1 * AUT,
+ reschedule => "skip",
+
+ on_tick => sub { $tick++ },
+ );
+
+ $loop->add( $timer );
+ $timer->start;
+
+ time_about( sub { wait_for { $tick == 1 } }, 1, 'skip Timer works' );
+
+ ok( $timer->is_running, 'skip Timer is still running' );
+
+ time_about( sub { wait_for { $tick == 2 } }, 1, 'skip Timer ticks a second time' );
+
+ $loop->remove( $timer );
+}
+
+# reschedule => "drift"
+{
+ my $tick = 0;
+
+ my $timer = IO::Async::Timer::Periodic->new(
+ interval => 1 * AUT,
+ reschedule => "drift",
+
+ on_tick => sub { $tick++ },
+ );
+
+ $loop->add( $timer );
+ $timer->start;
+
+ time_about( sub { wait_for { $tick == 1 } }, 1, 'drift Timer works' );
+
+ ok( $timer->is_running, 'drift Timer is still running' );
+
+ time_about( sub { wait_for { $tick == 2 } }, 1, 'drift Timer ticks a second time' );
+
+ $loop->remove( $timer );
+}
+
+# Self-stopping
+{
+ my $count = 0;
+ my $timer = IO::Async::Timer::Periodic->new(
+ interval => 0.1 * AUT,
+
+ on_tick => sub { $count++; shift->stop if $count >= 5 },
+ );
+
+ $loop->add( $timer );
+ $timer->start;
+
+ my $timedout;
+ my $id = $loop->watch_time( after => 1 * AUT, code => sub { $timedout++ } );
+
+ wait_for { $timedout };
+
+ is( $count, 5, 'Self-stopping timer can stop itself' );
+
+ $loop->remove( $timer );
+ $loop->unwatch_time( $id );
+}
+
+# Exception in on_tick shouldn't prevent reschedule
+{
+ my $count = 0;
+ my $timer = IO::Async::Timer::Periodic->new(
+ interval => 0.1 * AUT,
+
+ on_tick => sub { $count++; die "FAIL $count" },
+ );
+
+ $loop->add( $timer );
+ $timer->start;
+
+ like( exception { wait_for { $count > 0 } },
+ qr/FAIL 1/, 'on_tick death throws exception' );
+
+ like( exception { wait_for { $count > 1 } },
+ qr/FAIL 2/, 'on_tick death rescheduled and runs a second time' );
+
+ $loop->remove( $timer );
+}
+
+## Subclass
+
+my $sub_tick = 0;
+
+{
+ my $timer = TestTimer->new(
+ interval => 2 * AUT,
+ );
+
+ ok( defined $timer, 'subclass $timer defined' );
+ isa_ok( $timer, "IO::Async::Timer", 'subclass $timer isa IO::Async::Timer' );
+
+ is_oneref( $timer, 'subclass $timer has refcount 1 initially' );
+
+ $loop->add( $timer );
+
+ is_refcount( $timer, 2, 'subclass $timer has refcount 2 after adding to Loop' );
+
+ $timer->start;
+
+ is_refcount( $timer, 2, 'subclass $timer has refcount 2 after starting' );
+
+ ok( $timer->is_running, 'Started subclass Timer is running' );
+
+ time_about( sub { wait_for { $sub_tick == 1 } }, 2, 'subclass Timer works' );
+
+ is_refcount( $timer, 2, 'subclass $timer has refcount 2 before removing from Loop' );
+
+ $loop->remove( $timer );
+
+ is_oneref( $timer, 'subclass $timer has refcount 1 after removing from Loop' );
+}
+
+done_testing;
+
+package TestTimer;
+use base qw( IO::Async::Timer::Periodic );
+
+sub on_tick { $sub_tick++ }
diff --git a/t/23signal.t b/t/23signal.t
new file mode 100644
index 0000000..fc28642
--- /dev/null
+++ b/t/23signal.t
@@ -0,0 +1,148 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+use Test::Fatal;
+use Test::Refcount;
+
+use POSIX qw( SIGTERM );
+
+use IO::Async::Signal;
+
+use IO::Async::Loop;
+use IO::Async::OS;
+
+plan skip_all => "This OS does not have signals" unless IO::Async::OS->HAVE_SIGNALS;
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+my $caught = 0;
+
+my @rargs;
+
+my $signal = IO::Async::Signal->new(
+ name => 'TERM',
+ on_receipt => sub { @rargs = @_; $caught++ },
+);
+
+ok( defined $signal, '$signal defined' );
+isa_ok( $signal, "IO::Async::Signal", '$signal isa IO::Async::Signal' );
+
+is_oneref( $signal, '$signal has refcount 1 initially' );
+
+is( $signal->notifier_name, "TERM", '$signal->notifier_name' );
+
+$loop->add( $signal );
+
+is_refcount( $signal, 2, '$signal has refcount 2 after adding to Loop' );
+
+$loop->loop_once( 0.1 ); # nothing happens
+
+is( $caught, 0, '$caught idling' );
+
+kill SIGTERM, $$;
+
+wait_for { $caught };
+
+is( $caught, 1, '$caught after raise' );
+is_deeply( \@rargs, [ $signal ], 'on_receipt args after raise' );
+
+my $caught2 = 0;
+
+my $signal2 = IO::Async::Signal->new(
+ name => 'TERM',
+ on_receipt => sub { $caught2++ },
+);
+
+$loop->add( $signal2 );
+
+undef $caught;
+
+kill SIGTERM, $$;
+
+wait_for { $caught };
+
+is( $caught, 1, '$caught after raise' );
+is( $caught2, 1, '$caught2 after raise' );
+
+$loop->remove( $signal2 );
+
+undef $caught; undef $caught2;
+
+kill SIGTERM, $$;
+
+wait_for { $caught };
+
+is( $caught, 1, '$caught after raise' );
+is( $caught2, undef, '$caught2 after raise' );
+
+undef $caught;
+my $new_caught;
+$signal->configure( on_receipt => sub { $new_caught++ } );
+
+kill SIGTERM, $$;
+
+wait_for { $new_caught };
+
+is( $caught, undef, '$caught after raise after replace on_receipt' );
+is( $new_caught, 1, '$new_caught after raise after replace on_receipt' );
+
+undef @rargs;
+
+is_refcount( $signal, 2, '$signal has refcount 2 before removing from Loop' );
+
+$loop->remove( $signal );
+
+is_oneref( $signal, '$signal has refcount 1 finally' );
+
+undef $signal;
+
+## Subclass
+
+my $sub_caught = 0;
+
+$signal = TestSignal->new(
+ name => 'TERM',
+);
+
+ok( defined $signal, 'subclass $signal defined' );
+isa_ok( $signal, "IO::Async::Signal", 'subclass $signal isa IO::Async::Signal' );
+
+is_oneref( $signal, 'subclass $signal has refcount 1 initially' );
+
+$loop->add( $signal );
+
+is_refcount( $signal, 2, 'subclass $signal has refcount 2 after adding to Loop' );
+
+$loop->loop_once( 0.1 ); # nothing happens
+
+is( $sub_caught, 0, '$sub_caught idling' );
+
+kill SIGTERM, $$;
+
+wait_for { $sub_caught };
+
+is( $sub_caught, 1, '$sub_caught after raise' );
+
+ok( exception {
+ my $signal = IO::Async::Signal->new(
+ name => 'this signal name does not exist',
+ on_receipt => sub {},
+ );
+ $loop->add( $signal );
+ },
+ 'Bad signal name fails'
+);
+
+done_testing;
+
+package TestSignal;
+use base qw( IO::Async::Signal );
+
+sub on_receipt { $sub_caught++ }
diff --git a/t/24listener.t b/t/24listener.t
new file mode 100644
index 0000000..5a296aa
--- /dev/null
+++ b/t/24listener.t
@@ -0,0 +1,301 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+use Test::Identity;
+use Test::Refcount;
+
+use IO::Async::Loop;
+
+use IO::Socket::INET;
+
+use IO::Async::Listener;
+
+# Some odd locations like BSD jails might not like INADDR_ANY. We'll establish
+# a baseline first to test against
+my $INADDR_ANY = do {
+ my $anysock = IO::Socket::INET->new( LocalPort => 0, Listen => 1 );
+ $anysock->sockaddr;
+};
+my $INADDR_ANY_HOST = inet_ntoa( $INADDR_ANY );
+if( $INADDR_ANY ne INADDR_ANY ) {
+ diag( "Testing with INADDR_ANY=$INADDR_ANY_HOST; this may be because of odd networking" );
+}
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+my $listensock;
+
+$listensock = IO::Socket::INET->new(
+ LocalAddr => "localhost",
+ Type => SOCK_STREAM,
+ Listen => 1,
+) or die "Cannot socket() - $!";
+
+
+{
+ my $newclient;
+ my $listener = IO::Async::Listener->new(
+ handle => $listensock,
+ on_accept => sub { ( undef, $newclient ) = @_ },
+ );
+
+ ok( defined $listener, 'defined $listener' );
+ isa_ok( $listener, "IO::Async::Listener", '$listener isa IO::Async::Listener' );
+ isa_ok( $listener, "IO::Async::Notifier", '$listener isa IO::Async::Notifier' );
+
+ is_oneref( $listener, '$listener has refcount 1 initially' );
+
+ ok( $listener->is_listening, '$listener is_listening' );
+ is_deeply( [ unpack_sockaddr_in $listener->sockname ],
+ [ unpack_sockaddr_in $listensock->sockname ], '$listener->sockname' );
+
+ is( $listener->family, AF_INET, '$listener->family' );
+ is( $listener->socktype, SOCK_STREAM, '$listener->sockname' );
+
+ $loop->add( $listener );
+
+ is_refcount( $listener, 2, '$listener has refcount 2 after adding to Loop' );
+
+ my $clientsock = IO::Socket::INET->new( Type => SOCK_STREAM )
+ or die "Cannot socket() - $!";
+
+ $clientsock->connect( $listensock->sockname ) or die "Cannot connect() - $!";
+
+ ok( defined $clientsock->peername, '$clientsock is connected' );
+
+ wait_for { defined $newclient };
+
+ is_deeply( [ unpack_sockaddr_in $newclient->peername ],
+ [ unpack_sockaddr_in $clientsock->sockname ], '$newclient peer is correct' );
+
+ is_refcount( $listener, 2, '$listener has refcount 2 before removing from Loop' );
+
+ $loop->remove( $listener );
+
+ is_oneref( $listener, '$listener has refcount 1 after removing from Loop' );
+}
+
+# on_accept handle constructors
+{
+ my $accepted;
+ my $listener = IO::Async::Listener->new(
+ handle => $listensock,
+ on_accept => sub { ( undef, $accepted ) = @_ },
+ );
+
+ $loop->add( $listener );
+
+ require IO::Async::Stream;
+
+ # handle_constructor
+ {
+ $listener->configure( handle_constructor => sub {
+ return IO::Async::Stream->new;
+ } );
+
+ my $clientsock = IO::Socket::INET->new( Type => SOCK_STREAM )
+ or die "Cannot socket() - $!";
+
+ $clientsock->connect( $listensock->sockname ) or die "Cannot connect() - $!";
+
+ wait_for { defined $accepted };
+
+ isa_ok( $accepted, "IO::Async::Stream", '$accepted with handle_constructor' );
+ undef $accepted;
+ }
+
+ # handle_class
+ {
+ $listener->configure( handle_class => "IO::Async::Stream" );
+
+ my $clientsock = IO::Socket::INET->new( Type => SOCK_STREAM )
+ or die "Cannot socket() - $!";
+
+ $clientsock->connect( $listensock->sockname ) or die "Cannot connect() - $!";
+
+ wait_for { defined $accepted };
+
+ isa_ok( $accepted, "IO::Async::Stream", '$accepted with handle_constructor' );
+ undef $accepted;
+ }
+
+ $loop->remove( $listener );
+}
+
+# on_stream
+{
+ my $newstream;
+ my $listener = IO::Async::Listener->new(
+ handle => $listensock,
+ on_stream => sub { ( undef, $newstream ) = @_ },
+ );
+
+ $loop->add( $listener );
+
+ my $clientsock = IO::Socket::INET->new( Type => SOCK_STREAM )
+ or die "Cannot socket() - $!";
+
+ $clientsock->connect( $listensock->sockname ) or die "Cannot connect() - $!";
+
+ wait_for { defined $newstream };
+
+ isa_ok( $newstream, "IO::Async::Stream", 'on_stream $newstream isa IO::Async::Stream' );
+ is_deeply( [ unpack_sockaddr_in $newstream->read_handle->peername ],
+ [ unpack_sockaddr_in $clientsock->sockname ], '$newstream sock peer is correct' );
+
+ $loop->remove( $listener );
+}
+
+# on_socket
+{
+ my $newsocket;
+ my $listener = IO::Async::Listener->new(
+ handle => $listensock,
+ on_socket => sub { ( undef, $newsocket ) = @_ },
+ );
+
+ $loop->add( $listener );
+
+ my $clientsock = IO::Socket::INET->new( Type => SOCK_STREAM )
+ or die "Cannot socket() - $!";
+
+ $clientsock->connect( $listensock->sockname ) or die "Cannot connect() - $!";
+
+ wait_for { defined $newsocket };
+
+ isa_ok( $newsocket, "IO::Async::Socket", 'on_socket $newsocket isa IO::Async::Socket' );
+ is_deeply( [ unpack_sockaddr_in $newsocket->read_handle->peername ],
+ [ unpack_sockaddr_in $clientsock->sockname ], '$newsocket sock peer is correct' );
+
+ $loop->remove( $listener );
+}
+
+# Subclass
+{
+ my $sub_newclient;
+ {
+ package TestListener;
+ use base qw( IO::Async::Listener );
+
+ sub on_accept { ( undef, $sub_newclient ) = @_ }
+ }
+
+ my $listener = TestListener->new(
+ handle => $listensock,
+ );
+
+ ok( defined $listener, 'subclass defined $listener' );
+ isa_ok( $listener, "IO::Async::Listener", 'subclass $listener isa IO::Async::Listener' );
+
+ is_oneref( $listener, 'subclass $listener has refcount 1 initially' );
+
+ $loop->add( $listener );
+
+ is_refcount( $listener, 2, 'subclass $listener has refcount 2 after adding to Loop' );
+
+ my $clientsock = IO::Socket::INET->new( LocalAddr => "127.0.0.1", Type => SOCK_STREAM )
+ or die "Cannot socket() - $!";
+
+ $clientsock->connect( $listensock->sockname ) or die "Cannot connect() - $!";
+
+ ok( defined $clientsock->peername, 'subclass $clientsock is connected' );
+
+ wait_for { defined $sub_newclient };
+
+ is_deeply( [ unpack_sockaddr_in $sub_newclient->peername ],
+ [ unpack_sockaddr_in $clientsock->sockname ], '$sub_newclient peer is correct' );
+
+ is_refcount( $listener, 2, 'subclass $listener has refcount 2 before removing from Loop' );
+
+ $loop->remove( $listener );
+
+ is_oneref( $listener, 'subclass $listener has refcount 1 after removing from Loop' );
+}
+
+# Subclass with handle_constructor
+{
+ {
+ package TestListener::WithConstructor;
+ use base qw( IO::Async::Listener );
+
+ sub handle_constructor { return IO::Async::Stream->new }
+ }
+
+ my $accepted;
+
+ my $listener = TestListener::WithConstructor->new(
+ handle => $listensock,
+ on_accept => sub { ( undef, $accepted ) = @_; },
+ );
+
+ $loop->add( $listener );
+
+ my $clientsock = IO::Socket::INET->new( Type => SOCK_STREAM )
+ or die "Cannot socket() - $!";
+
+ $clientsock->connect( $listensock->sockname ) or die "Cannot connect() - $!";
+
+ wait_for { defined $accepted };
+
+ isa_ok( $accepted, "IO::Async::Stream", '$accepted with handle_constructor method' );
+
+ $loop->remove( $listener );
+}
+
+{
+ my $newclient;
+ my $listener = IO::Async::Listener->new(
+ on_accept => sub { ( undef, $newclient ) = @_ },
+ );
+
+ ok( !$listener->is_listening, '$listener is_listening not yet' );
+
+ $loop->add( $listener );
+
+ my $listen_self;
+
+ $listener->listen(
+ addr => { family => "inet", socktype => "stream", addr => pack_sockaddr_in( 0, $INADDR_ANY ) },
+ on_listen => sub { $listen_self = shift },
+ on_listen_error => sub { die "Test died early - $_[0] - $_[-1]\n"; },
+ );
+
+ ok( $listener->is_listening, '$listener is_listening' );
+
+ my $sockname = $listener->sockname;
+ ok( defined $sockname, 'defined $sockname' );
+
+ my ( $port, $sinaddr ) = unpack_sockaddr_in( $sockname );
+
+ ok( $port > 0, 'socket listens on some defined port number' );
+ is( inet_ntoa( $sinaddr ), $INADDR_ANY_HOST, 'socket listens on INADDR_ANY' );
+
+ is( $listener->family, AF_INET, '$listener->family' );
+ is( $listener->socktype, SOCK_STREAM, '$listener->sockname' );
+
+ is( $listen_self, $listener, '$listen_self is $listener' );
+ undef $listen_self; # for refcount
+
+ my $clientsock = IO::Socket::INET->new( Type => SOCK_STREAM )
+ or die "Cannot socket() - $!";
+
+ $clientsock->connect( pack_sockaddr_in( $port, INADDR_LOOPBACK ) ) or die "Cannot connect() - $!";
+
+ ok( defined $clientsock->peername, '$clientsock is connected' );
+
+ wait_for { defined $newclient };
+
+ is_deeply( [ unpack_sockaddr_in $newclient->peername ],
+ [ unpack_sockaddr_in $clientsock->sockname ], '$newclient peer is correct' );
+
+ $loop->remove( $listener );
+}
+
+done_testing;
diff --git a/t/25socket.t b/t/25socket.t
new file mode 100644
index 0000000..8da852f
--- /dev/null
+++ b/t/25socket.t
@@ -0,0 +1,325 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+use Test::Fatal;
+use Test::Refcount;
+
+use Errno qw( EAGAIN EWOULDBLOCK ECONNRESET );
+
+use Socket qw( unpack_sockaddr_in );
+
+use IO::Async::Loop;
+
+use IO::Async::OS;
+
+use IO::Async::Socket;
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+# useful test function
+sub recv_data
+{
+ my ( $s ) = @_;
+
+ my $buffer;
+ my $ret = $s->recv( $buffer, 8192 );
+
+ return $buffer if defined $ret and length $buffer;
+ die "Socket closed" if defined $ret;
+ return "" if $! == EAGAIN or $! == EWOULDBLOCK;
+ die "Cannot recv - $!";
+}
+
+ok( !exception { IO::Async::Socket->new( write_handle => \*STDOUT ) }, 'Send-only Socket works' );
+
+# Receiving
+{
+ my ( $S1, $S2 ) = IO::Async::OS->socketpair( "inet", "dgram" ) or die "Cannot socketpair - $!";
+ my @S2addr = unpack_sockaddr_in $S2->sockname;
+
+ # Need sockets in nonblocking mode
+ $S1->blocking( 0 );
+ $S2->blocking( 0 );
+
+ my @received;
+
+ my $socket = IO::Async::Socket->new(
+ handle => $S1,
+ on_recv => sub {
+ my $self = shift;
+ my ( $dgram, $sender ) = @_;
+
+ push @received, [ $dgram, unpack_sockaddr_in $sender ];
+ },
+ );
+
+ ok( defined $socket, 'recving $socket defined' );
+ isa_ok( $socket, "IO::Async::Socket", 'recving $socket isa IO::Async::Socket' );
+
+ is_oneref( $socket, 'recving $socket has refcount 1 initially' );
+
+ $loop->add( $socket );
+
+ is_refcount( $socket, 2, 'recving $socket has refcount 2 after adding to Loop' );
+
+ $S2->send( "message\n" );
+
+ is_deeply( \@received, [], '@received before wait' );
+
+ wait_for { scalar @received };
+
+ is_deeply( \@received,
+ [ [ "message\n", @S2addr ] ],
+ '@received after wait' );
+
+ undef @received;
+ my @new_received;
+ $socket->configure(
+ on_recv => sub {
+ my $self = shift;
+ my ( $dgram, $sender ) = @_;
+ push @new_received, [ $dgram, unpack_sockaddr_in $sender ];
+ },
+ );
+
+ $S2->send( "another message\n" );
+
+ wait_for { scalar @new_received };
+
+ is( scalar @received, 0, '@received still empty after on_recv replace' );
+ is_deeply( \@new_received,
+ [ [ "another message\n", @S2addr ] ],
+ '@new_received after on_recv replace' );
+
+ is_refcount( $socket, 2, 'receiving $socket has refcount 2 before removing from Loop' );
+
+ $loop->remove( $socket );
+
+ is_oneref( $socket, 'receiving $socket refcount 1 finally' );
+}
+
+SKIP: {
+ # Don't bother with an OS constant for this as it's only used by this unit-test
+ skip "This OS cannot safely ->recv with truncation", 3 if $^O eq "MSWin32";
+
+ my ( $S1, $S2 ) = IO::Async::OS->socketpair( "inet", "dgram" ) or die "Cannot socketpair - $!";
+
+ # Need sockets in nonblocking mode
+ $S1->blocking( 0 );
+ $S2->blocking( 0 );
+
+ my @frags;
+ my $socket = IO::Async::Socket->new(
+ handle => $S1,
+ recv_len => 4,
+ on_recv => sub {
+ my ( $self, $dgram ) = @_;
+ push @frags, $dgram;
+ },
+ );
+
+ $loop->add( $socket );
+
+ $S2->send( "A nice long message" );
+ $S2->send( "another one here" );
+ $S2->send( "and again" );
+
+ wait_for { scalar @frags };
+
+ is_deeply( \@frags, [ "A ni" ], '@frags with recv_len=4 without recv_all' );
+
+ wait_for { @frags == 3 };
+
+ is_deeply( \@frags, [ "A ni", "anot", "and " ], '@frags finally with recv_len=4 without recv_all' );
+
+ undef @frags;
+ $socket->configure( recv_all => 1 );
+
+ $S2->send( "Long messages" );
+ $S2->send( "Repeated" );
+ $S2->send( "Once more" );
+
+ wait_for { scalar @frags };
+
+ is_deeply( \@frags, [ "Long", "Repe", "Once" ], '@frags with recv_len=4 with recv_all' );
+
+ $loop->remove( $socket );
+}
+
+{
+ my ( $S1, $S2 ) = IO::Async::OS->socketpair( "inet", "dgram" ) or die "Cannot socketpair - $!";
+
+ my $no_on_recv_socket;
+ ok( !exception { $no_on_recv_socket = IO::Async::Socket->new( handle => $S1 ) },
+ 'Allowed to construct a Socket without an on_recv handler' );
+ ok( exception { $loop->add( $no_on_recv_socket ) },
+ 'Not allowed to add an on_recv-less Socket to a Loop' );
+ }
+
+# Subclass
+
+my @sub_received;
+
+{
+ my ( $S1, $S2 ) = IO::Async::OS->socketpair( "inet", "dgram" ) or die "Cannot socketpair - $!";
+ my @S2addr = unpack_sockaddr_in $S2->sockname;
+
+ # Need sockets in nonblocking mode
+ $S1->blocking( 0 );
+ $S2->blocking( 0 );
+
+ my $socket = TestSocket->new(
+ handle => $S1,
+ );
+
+ ok( defined $socket, 'receiving subclass $socket defined' );
+ isa_ok( $socket, "IO::Async::Socket", 'receiving $socket isa IO::Async::Socket' );
+
+ is_oneref( $socket, 'subclass $socket has refcount 1 initially' );
+
+ $loop->add( $socket );
+
+ is_refcount( $socket, 2, 'subclass $socket has refcount 2 after adding to Loop' );
+
+ $S2->send( "message\n" );
+
+ is_deeply( \@sub_received, [], '@sub_received before wait' );
+
+ wait_for { scalar @sub_received };
+
+ is_deeply( \@sub_received,
+ [ [ "message\n", @S2addr ] ],
+ '@sub_received after wait' );
+
+ $loop->remove( $socket );
+}
+
+# Sending
+{
+ my ( $S1, $S2 ) = IO::Async::OS->socketpair( "inet", "dgram" ) or die "Cannot socketpair - $!";
+
+ # Need sockets in nonblocking mode
+ $S1->blocking( 0 );
+ $S2->blocking( 0 );
+
+ my $empty;
+
+ my $socket = IO::Async::Socket->new(
+ write_handle => $S1,
+ on_outgoing_empty => sub { $empty = 1 },
+ );
+
+ ok( defined $socket, 'sending $socket defined' );
+ isa_ok( $socket, "IO::Async::Socket", 'sending $socket isa IO::Async::Socket' );
+
+ is_oneref( $socket, 'sending $socket has refcount 1 intially' );
+
+ $loop->add( $socket );
+
+ is_refcount( $socket, 2, 'sending $socket has refcount 2 after adding to Loop' );
+
+ ok( !$socket->want_writeready, 'want_writeready before send' );
+ $socket->send( "message\n" );
+
+ ok( $socket->want_writeready, 'want_writeready after send' );
+
+ wait_for { $empty };
+
+ ok( !$socket->want_writeready, 'want_writeready after wait' );
+ is( $empty, 1, '$empty after writing buffer' );
+
+ is( recv_data( $S2 ), "message\n", 'data after writing buffer' );
+
+ $socket->configure( autoflush => 1 );
+ $socket->send( "immediate\n" );
+
+ ok( !$socket->want_writeready, 'not want_writeready after autoflush send' );
+ is( recv_data( $S2 ), "immediate\n", 'data after autoflush send' );
+
+ $socket->configure( autoflush => 0 );
+ $socket->send( "First\n" );
+ $socket->configure( autoflush => 1 );
+ $socket->send( "Second\n" );
+
+ ok( !$socket->want_writeready, 'not want_writeready after split autoflush send' );
+ is( recv_data( $S2 ), "First\n", 'data[0] after split autoflush send' );
+ is( recv_data( $S2 ), "Second\n", 'data[1] after split autoflush send' );
+
+ is_refcount( $socket, 2, 'sending $socket has refcount 2 before removing from Loop' );
+
+ $loop->remove( $socket );
+
+ is_oneref( $socket, 'sending $socket has refcount 1 finally' );
+}
+
+# Socket errors
+{
+ my ( $ES1, $ES2 ) = IO::Async::OS->socketpair or die "Cannot socketpair - $!";
+ $ES2->syswrite( "X" ); # ensuring $ES1 is read- and write-ready
+ # cheating and hackery
+ bless $ES1, "ErrorSocket";
+
+ $ErrorSocket::errno = ECONNRESET;
+
+ my $recv_errno;
+ my $send_errno;
+
+ my $socket = IO::Async::Socket->new(
+ read_handle => $ES1,
+ on_recv => sub {},
+ on_recv_error => sub { ( undef, $recv_errno ) = @_ },
+ );
+
+ $loop->add( $socket );
+
+ wait_for { defined $recv_errno };
+
+ cmp_ok( $recv_errno, "==", ECONNRESET, 'errno after failed recv' );
+
+ $loop->remove( $socket );
+
+ $socket = IO::Async::Socket->new(
+ write_handle => $ES1,
+ on_send_error => sub { ( undef, $send_errno ) = @_ },
+ );
+
+ $loop->add( $socket );
+
+ $socket->send( "hello" );
+
+ wait_for { defined $send_errno };
+
+ cmp_ok( $send_errno, "==", ECONNRESET, 'errno after failed send' );
+
+ $loop->remove( $socket );
+}
+
+done_testing;
+
+package TestSocket;
+use base qw( IO::Async::Socket );
+use Socket qw( unpack_sockaddr_in );
+
+sub on_recv
+{
+ my $self = shift;
+ my ( $dgram, $sender ) = @_;
+
+ push @sub_received, [ $dgram, unpack_sockaddr_in $sender ];
+}
+
+package ErrorSocket;
+
+use base qw( IO::Socket );
+our $errno;
+
+sub recv { $! = $errno; undef; }
+sub send { $! = $errno; undef; }
+sub close { }
diff --git a/t/26pid.t b/t/26pid.t
new file mode 100644
index 0000000..cdd770b
--- /dev/null
+++ b/t/26pid.t
@@ -0,0 +1,89 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+use Test::Refcount;
+
+use POSIX qw( SIGTERM );
+
+use IO::Async::PID;
+
+use IO::Async::Loop;
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+{
+ my $kid = fork;
+ defined $kid or die "Cannot fork() - $!";
+
+ if( $kid == 0 ) {
+ # child
+ exit( 3 );
+ # this exists as a zombie for now, but we'll deal with this later
+ }
+
+ my $exitcode;
+ my $pid = IO::Async::PID->new(
+ pid => $kid,
+ on_exit => sub { ( undef, $exitcode ) = @_; }
+ );
+
+ ok( defined $pid, '$pid defined' );
+ isa_ok( $pid, "IO::Async::PID", '$pid isa IO::Async::PID' );
+
+ is_oneref( $pid, '$pid has refcount 1 initially' );
+
+ is( $pid->pid, $kid, '$pid->pid' );
+
+ is( $pid->notifier_name, "$kid", '$pid->notifier_name' );
+
+ $loop->add( $pid );
+
+ is_refcount( $pid, 2, '$pid has refcount 2 after adding to Loop' );
+
+ # reap zombie
+ wait_for { defined $exitcode };
+
+ ok( ($exitcode & 0x7f) == 0, 'WIFEXITED($exitcode) after process exit' );
+ is( ($exitcode >> 8), 3, 'WEXITSTATUS($exitcode) after process exit' );
+}
+
+SKIP: {
+ skip "This OS has no signals", 1 unless IO::Async::OS->HAVE_SIGNALS;
+
+ # We require that SIGTERM perform its default action; i.e. terminate the
+ # process. Ensure this definitely happens, in case the test harness has it
+ # ignored or handled elsewhere.
+ local $SIG{TERM} = "DEFAULT";
+
+ my $kid = fork;
+ defined $kid or die "Cannot fork() - $!";
+
+ if( $kid == 0 ) {
+ sleep( 10 );
+ # Just in case the parent died already and didn't kill us
+ exit( 0 );
+ }
+
+ my $exitcode;
+ my $pid = IO::Async::PID->new(
+ pid => $kid,
+ on_exit => sub { ( undef, $exitcode ) = @_; }
+ );
+
+ $loop->add( $pid );
+
+ $pid->kill( SIGTERM );
+
+ wait_for { defined $exitcode };
+
+ is( ($exitcode & 0x7f), SIGTERM, 'WTERMSIG($exitcode) after SIGTERM' );
+}
+
+done_testing;
diff --git a/t/27file.t b/t/27file.t
new file mode 100644
index 0000000..6c79ff7
--- /dev/null
+++ b/t/27file.t
@@ -0,0 +1,113 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+use Test::Refcount;
+
+use Fcntl qw( SEEK_SET SEEK_END );
+use File::Temp qw( tempfile );
+
+use IO::Async::Loop;
+
+use IO::Async::OS;
+
+use IO::Async::File;
+
+use constant AUT => $ENV{TEST_QUICK_TIMERS} ? 0.1 : 1;
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+sub mkhandles
+{
+ my ( $rd, $filename ) = tempfile( "tmpfile.XXXXXX", UNLINK => 1 );
+ open my $wr, ">", $filename or die "Cannot reopen file for writing - $!";
+
+ $wr->autoflush( 1 );
+
+ return ( $rd, $wr, $filename );
+}
+
+{
+ my ( $rd, $wr ) = mkhandles;
+
+ my $size_change;
+ my ( $new_size, $old_size );
+ my ( $new_stat, $old_stat );
+ my $file = IO::Async::File->new(
+ interval => 0.1 * AUT,
+ handle => $rd,
+ on_size_changed => sub {
+ ( undef, $new_size, $old_size ) = @_;
+ $size_change++;
+ },
+ on_stat_changed => sub {
+ ( undef, $new_stat, $old_stat ) = @_;
+ },
+ );
+
+ ok( defined $file, '$file defined' );
+ isa_ok( $file, "IO::Async::File", '$file isa IO::Async::File' );
+
+ is_oneref( $file, '$file has refcount 1 initially' );
+
+ is( $file->handle, $rd, '$file->handle is $rd' );
+
+ $loop->add( $file );
+
+ is_refcount( $file, 2, '$file has refcount 2 after adding to Loop' );
+
+ $wr->syswrite( "message\n" );
+
+ wait_for { $size_change };
+
+ is( $old_size, 0, '$old_size' );
+ is( $new_size, 8, '$new_size' );
+
+ isa_ok( $old_stat, "File::stat", '$old_stat isa File::stat' );
+ isa_ok( $new_stat, "File::stat", '$new_stat isa File::stat' );
+
+ $loop->remove( $file );
+}
+
+# Follow by name
+SKIP: {
+ skip "OS is unable to rename open files", 3 unless IO::Async::OS->HAVE_RENAME_OPEN_FILES;
+
+ my ( undef, $wr, $filename ) = mkhandles;
+
+ my $devino_changed;
+ my ( $old_stat, $new_stat );
+ my $file = IO::Async::File->new(
+ interval => 0.1 * AUT,
+ filename => $filename,
+ on_devino_changed => sub {
+ ( undef, $new_stat, $old_stat ) = @_;
+ $devino_changed++;
+ },
+ );
+
+ ok( $file->handle, '$file has a ->handle' );
+
+ $loop->add( $file );
+
+ close $wr;
+ rename( $filename, "$filename.old" ) or die "Cannot rename $filename - $!";
+ END { defined $filename and -f $filename and unlink $filename }
+ END { defined $filename and -f "$filename.old" and unlink "$filename.old" }
+ open $wr, ">", $filename or die "Cannot reopen $filename for writing - $!";
+
+ wait_for { $devino_changed };
+
+ is( $new_stat->dev, (stat $wr)[0], '$new_stat->dev for renamed file' );
+ is( $new_stat->ino, (stat $wr)[1], '$new_stat->ino for renamed file' );
+
+ $loop->remove( $file );
+}
+
+done_testing;
diff --git a/t/28filestream.t b/t/28filestream.t
new file mode 100644
index 0000000..f51887e
--- /dev/null
+++ b/t/28filestream.t
@@ -0,0 +1,323 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+use Test::Fatal;
+use Test::Refcount;
+
+use Fcntl qw( SEEK_SET SEEK_END );
+use File::Temp qw( tempfile );
+
+use IO::Async::Loop;
+
+use IO::Async::OS;
+
+use IO::Async::FileStream;
+
+use constant AUT => $ENV{TEST_QUICK_TIMERS} ? 0.1 : 1;
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+sub mkhandles
+{
+ my ( $rd, $filename ) = tempfile( "tmpfile.XXXXXX", UNLINK => 1 );
+ open my $wr, ">", $filename or die "Cannot reopen file for writing - $!";
+
+ $wr->autoflush( 1 );
+
+ return ( $rd, $wr, $filename );
+}
+
+{
+ my ( $rd, $wr ) = mkhandles;
+
+ my @lines;
+ my $initial_size;
+
+ my $filestream = IO::Async::FileStream->new(
+ interval => 0.1 * AUT,
+ read_handle => $rd,
+ on_read => sub {
+ my $self = shift;
+ my ( $buffref, $eof ) = @_;
+
+ push @lines, $1 while $$buffref =~ s/^(.*\n)//;
+ return 0;
+ },
+ on_initial => sub { ( undef, $initial_size ) = @_ },
+ );
+
+ ok( defined $filestream, '$filestream defined' );
+ isa_ok( $filestream, "IO::Async::FileStream", '$filestream isa IO::Async::FileStream' );
+
+ is_oneref( $filestream, 'reading $filestream has refcount 1 initially' );
+
+ $loop->add( $filestream );
+
+ is_refcount( $filestream, 2, '$filestream has refcount 2 after adding to Loop' );
+
+ is( $initial_size, 0, '$initial_size is 0' );
+
+ $wr->syswrite( "message\n" );
+
+ is_deeply( \@lines, [], '@lines before wait' );
+
+ wait_for { scalar @lines };
+
+ is_deeply( \@lines, [ "message\n" ], '@lines after wait' );
+
+ $loop->remove( $filestream );
+}
+
+# on_initial
+{
+ my ( $rd, $wr ) = mkhandles;
+
+ $wr->syswrite( "Some initial content\n" );
+
+ my @lines;
+ my $initial_size;
+
+ my $filestream = IO::Async::FileStream->new(
+ interval => 0.1 * AUT,
+ read_handle => $rd,
+ on_read => sub {
+ my $self = shift;
+ my ( $buffref, $eof ) = @_;
+
+ push @lines, $1 while $$buffref =~ s/^(.*\n)//;
+ return 0;
+ },
+ on_initial => sub { ( undef, $initial_size ) = @_ },
+ );
+
+ $loop->add( $filestream );
+
+ is( $initial_size, 21, '$initial_size is 21' );
+
+ $wr->syswrite( "More content\n" );
+
+ wait_for { scalar @lines };
+
+ is_deeply( \@lines, [ "Some initial content\n", "More content\n" ], 'All content is visible' );
+
+ $loop->remove( $filestream );
+}
+
+# seek_to_last
+{
+ my ( $rd, $wr ) = mkhandles;
+
+ $wr->syswrite( "Some skipped content\nWith a partial line" );
+
+ my @lines;
+
+ my $filestream = IO::Async::FileStream->new(
+ interval => 0.1 * AUT,
+ read_handle => $rd,
+ on_read => sub {
+ my $self = shift;
+ my ( $buffref, $eof ) = @_;
+
+ return 0 unless( $$buffref =~ s/^(.*\n)// );
+
+ push @lines, $1;
+ return 1;
+ },
+ on_initial => sub {
+ my $self = shift;
+ # Give it a tiny block size, forcing it to have to seek harder to find the \n
+ ok( $self->seek_to_last( "\n", blocksize => 8 ), 'FileStream successfully seeks to last \n' );
+ },
+ );
+
+ $loop->add( $filestream );
+
+ $wr->syswrite( " finished here\n" );
+
+ wait_for { scalar @lines };
+
+ is_deeply( \@lines, [ "With a partial line finished here\n" ], 'Partial line completely returned' );
+
+ $loop->remove( $filestream );
+}
+
+# on_initial can skip content
+{
+ my ( $rd, $wr ) = mkhandles;
+
+ $wr->syswrite( "Some skipped content\n" );
+
+ my @lines;
+
+ my $filestream = IO::Async::FileStream->new(
+ interval => 0.1 * AUT,
+ read_handle => $rd,
+ on_read => sub {
+ my $self = shift;
+ my ( $buffref, $eof ) = @_;
+
+ return 0 unless( $$buffref =~ s/^(.*\n)// );
+
+ push @lines, $1;
+ return 1;
+ },
+ on_initial => sub { my $self = shift; $self->seek( 0, SEEK_END ); },
+ );
+
+ $loop->add( $filestream );
+
+ $wr->syswrite( "Additional content\n" );
+
+ wait_for { scalar @lines };
+
+ is_deeply( \@lines, [ "Additional content\n" ], 'Initial content is skipped' );
+
+ $loop->remove( $filestream );
+}
+
+# Truncation
+{
+ my ( $rd, $wr ) = mkhandles;
+
+ my @lines;
+ my $truncated;
+
+ my $filestream = IO::Async::FileStream->new(
+ interval => 0.1 * AUT,
+ read_handle => $rd,
+ on_read => sub {
+ my $self = shift;
+ my ( $buffref, $eof ) = @_;
+
+ return 0 unless( $$buffref =~ s/^(.*\n)// );
+
+ push @lines, $1;
+ return 1;
+ },
+ on_truncated => sub { $truncated++ },
+ );
+
+ $loop->add( $filestream );
+
+ $wr->syswrite( "Some original lines\nin the file\n" );
+
+ wait_for { scalar @lines };
+
+ $wr->truncate( 0 );
+ sysseek( $wr, 0, SEEK_SET );
+ $wr->syswrite( "And another\n" );
+
+ wait_for { @lines == 3 };
+
+ is( $truncated, 1, 'File content truncation detected' );
+ is_deeply( \@lines,
+ [ "Some original lines\n", "in the file\n", "And another\n" ],
+ 'All three lines read' );
+
+ $loop->remove( $filestream );
+}
+
+# Follow by name
+SKIP: {
+ skip "OS is unable to rename open files", 7 unless IO::Async::OS->HAVE_RENAME_OPEN_FILES;
+
+ my ( undef, $wr, $filename ) = mkhandles;
+
+ my @lines;
+
+ my $filestream = IO::Async::FileStream->new(
+ interval => 0.1 * AUT,
+ filename => $filename,
+ on_read => sub {
+ my $self = shift;
+ my ( $buffref, $eof ) = @_;
+
+ push @lines, $1 while $$buffref =~ s/^(.*\n)//;
+ return 0;
+ },
+ );
+
+ ok( defined $filestream, '$filestream defined for filenaem' );
+ isa_ok( $filestream, "IO::Async::FileStream", '$filestream isa IO::Async::FileStream' );
+
+ is_oneref( $filestream, 'reading $filestream has refcount 1 initially' );
+
+ $loop->add( $filestream );
+
+ is_refcount( $filestream, 2, '$filestream has refcount 2 after adding to Loop' );
+
+ $wr->syswrite( "message\n" );
+ wait_for { scalar @lines };
+
+ is_deeply( \@lines, [ "message\n" ], '@lines after wait' );
+ shift @lines;
+
+ $wr->syswrite( "last line of old file\n" );
+ close $wr;
+ rename( $filename, "$filename.old" ) or die "Cannot rename $filename - $!";
+ END { defined $filename and -f $filename and unlink $filename }
+ END { defined $filename and -f "$filename.old" and unlink "$filename.old" }
+ open $wr, ">", $filename or die "Cannot reopen $filename for writing - $!";
+ $wr->syswrite( "first line of new file\n" );
+
+ wait_for { scalar @lines };
+ is_deeply( $lines[0], "last line of old file\n", '@lines sees last line of old file' );
+ wait_for { scalar @lines >= 2 };
+ is_deeply( $lines[1], "first line of new file\n", '@lines sees first line of new file' );
+
+ $loop->remove( $filestream );
+}
+
+# Subclass
+my @sub_lines;
+
+{
+ my ( $rd, $wr ) = mkhandles;
+
+ my $filestream = TestStream->new(
+ interval => 0.1 * AUT,
+ read_handle => $rd,
+ );
+
+ ok( defined $filestream, 'subclass $filestream defined' );
+ isa_ok( $filestream, "IO::Async::FileStream", '$filestream isa IO::Async::FileStream' );
+
+ is_oneref( $filestream, 'subclass $filestream has refcount 1 initially' );
+
+ $loop->add( $filestream );
+
+ is_refcount( $filestream, 2, 'subclass $filestream has refcount 2 after adding to Loop' );
+
+ $wr->syswrite( "message\n" );
+
+ is_deeply( \@sub_lines, [], '@sub_lines before wait' );
+
+ wait_for { scalar @sub_lines };
+
+ is_deeply( \@sub_lines, [ "message\n" ], '@sub_lines after wait' );
+
+ $loop->remove( $filestream );
+}
+
+done_testing;
+
+package TestStream;
+use base qw( IO::Async::FileStream );
+
+sub on_read
+{
+ my $self = shift;
+ my ( $buffref ) = @_;
+
+ return 0 unless $$buffref =~ s/^(.*\n)//;
+
+ push @sub_lines, $1;
+ return 1;
+}
diff --git a/t/30loop-fork.t b/t/30loop-fork.t
new file mode 100644
index 0000000..927b6c9
--- /dev/null
+++ b/t/30loop-fork.t
@@ -0,0 +1,81 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+
+use POSIX qw( SIGINT );
+
+use IO::Async::Loop;
+use IO::Async::OS;
+
+plan skip_all => "POSIX fork() is not available" unless IO::Async::OS->HAVE_POSIX_FORK;
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+{
+ my $exitcode;
+ $loop->fork(
+ code => sub { return 5; },
+ on_exit => sub { ( undef, $exitcode ) = @_ },
+ );
+
+ wait_for { defined $exitcode };
+
+ ok( ($exitcode & 0x7f) == 0, 'WIFEXITED($exitcode) after child exit' );
+ is( ($exitcode >> 8), 5, 'WEXITSTATUS($exitcode) after child exit' );
+}
+
+{
+ my $exitcode;
+ $loop->fork(
+ code => sub { die "error"; },
+ on_exit => sub { ( undef, $exitcode ) = @_ },
+ );
+
+ wait_for { defined $exitcode };
+
+ ok( ($exitcode & 0x7f) == 0, 'WIFEXITED($exitcode) after child die' );
+ is( ($exitcode >> 8), 255, 'WEXITSTATUS($exitcode) after child die' );
+}
+
+SKIP: {
+ skip "This OS does not have signals", 1 unless IO::Async::OS->HAVE_SIGNALS;
+
+ local $SIG{INT} = sub { exit( 22 ) };
+
+ my $exitcode;
+ $loop->fork(
+ code => sub { kill SIGINT, $$ },
+ on_exit => sub { ( undef, $exitcode ) = @_ },
+ );
+
+ wait_for { defined $exitcode };
+
+ is( ($exitcode & 0x7f), SIGINT, 'WTERMSIG($exitcode) after child SIGINT' );
+}
+
+SKIP: {
+ skip "This OS does not have signals", 2 unless IO::Async::OS->HAVE_SIGNALS;
+
+ local $SIG{INT} = sub { exit( 22 ) };
+
+ my $exitcode;
+ $loop->fork(
+ code => sub { kill SIGINT, $$ },
+ on_exit => sub { ( undef, $exitcode ) = @_ },
+ keep_signals => 1,
+ );
+
+ wait_for { defined $exitcode };
+
+ ok( ($exitcode & 0x7f) == 0, 'WIFEXITED($exitcode) after child SIGINT with keep_signals' );
+ is( ($exitcode >> 8), 22, 'WEXITSTATUS($exitcode) after child SIGINT with keep_signals' );
+}
+
+done_testing;
diff --git a/t/31loop-spawnchild.t b/t/31loop-spawnchild.t
new file mode 100644
index 0000000..1cac1d9
--- /dev/null
+++ b/t/31loop-spawnchild.t
@@ -0,0 +1,168 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+use Test::Fatal;
+
+use POSIX qw( ENOENT EBADF );
+
+use IO::Async::OS;
+
+plan skip_all => "POSIX fork() is not available" unless IO::Async::OS->HAVE_POSIX_FORK;
+
+use IO::Async::Loop;
+
+# Need to look this up, so we don't hardcode the message in the test script
+# This might cause locale issues
+use constant ENOENT_MESSAGE => do { local $! = ENOENT; "$!" };
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+ok( exception { $loop->spawn_child( badoption => 1 ); }, 'Bad option to spawn fails' );
+
+ok( exception { $loop->spawn_child( code => sub { 1 }, command => "hello" ); },
+ 'Both code and command options to spawn fails' );
+
+ok( exception { $loop->spawn_child( on_exit => sub { 1 } ); }, 'Bad option to spawn fails' );
+
+{
+ my ( $exited_pid, $exitcode, $dollarbang, $dollarat );
+ my $spawned_pid = $loop->spawn_child(
+ code => sub { return 42; },
+ on_exit => sub { ( $exited_pid, $exitcode, $dollarbang, $dollarat ) = @_; }
+ );
+
+ wait_for { defined $exited_pid };
+
+ is( $exited_pid, $spawned_pid, '$exited_pid == $spawned_pid after spawn CODE' );
+ ok( ($exitcode & 0x7f) == 0, 'WIFEXITED($exitcode) after spawn CODE' );
+ is( ($exitcode >> 8), 42, 'WEXITSTATUS($exitcode) after spawn CODE' );
+ # dollarbang isn't interesting here
+ is( $dollarat, '', '$dollarat after spawn CODE' );
+}
+
+my $ENDEXIT = 10;
+END { exit $ENDEXIT if defined $ENDEXIT; }
+
+{
+ my ( $exited_pid, $exitcode, $dollarbang, $dollarat );
+ my $spawned_pid = $loop->spawn_child(
+ code => sub { return 0; },
+ on_exit => sub { ( $exited_pid, $exitcode, $dollarbang, $dollarat ) = @_; }
+ );
+
+ wait_for { defined $exited_pid };
+
+ is( $exited_pid, $spawned_pid, '$exited_pid == $spawned_pid after spawn CODE with END' );
+ ok( ($exitcode & 0x7f) == 0, 'WIFEXITED($exitcode) after spawn CODE with END' );
+ # If this comes out as 10 then the END block ran and we fail.
+ is( ($exitcode >> 8), 0, 'WEXITSTATUS($exitcode) after spawn CODE with END' );
+ # dollarbang isn't interesting here
+ is( $dollarat, '', '$dollarat after spawn CODE with END' );
+}
+
+{
+ my ( $exited_pid, $exitcode, $dollarbang, $dollarat );
+ my $spawned_pid = $loop->spawn_child(
+ code => sub { die "An exception here\n"; },
+ on_exit => sub { ( $exited_pid, $exitcode, $dollarbang, $dollarat ) = @_; }
+ );
+
+ wait_for { defined $exited_pid };
+
+ is( $exited_pid, $spawned_pid, '$exited_pid == $spawned_pid after spawn CODE with die with END' );
+ ok( ($exitcode & 0x7f) == 0, 'WIFEXITED($exitcode) after spawn CODE with die with END' );
+ is( ($exitcode >> 8), 255, 'WEXITSTATUS($exitcode) after spawn CODE with die with END' );
+ # dollarbang isn't interesting here
+ is( $dollarat, "An exception here\n", '$dollarat after spawn CODE with die with END' );
+}
+
+undef $ENDEXIT;
+
+# We need a command that just exits immediately with 0
+my $true;
+foreach (qw( /bin/true /usr/bin/true )) {
+ $true = $_, last if -x $_;
+}
+
+# Didn't find a likely-looking candidate. We'll fake one using perl itself
+$true = "$^X -e 1" if !defined $true;
+
+{
+ my ( $exited_pid, $exitcode, $dollarbang, $dollarat );
+ my $spawned_pid = $loop->spawn_child(
+ command => $true,
+ on_exit => sub { ( $exited_pid, $exitcode, $dollarbang, $dollarat ) = @_; }
+ );
+
+ wait_for { defined $exited_pid };
+
+ is( $exited_pid, $spawned_pid, '$exited_pid == $spawned_pid after spawn '.$true );
+ ok( ($exitcode & 0x7f) == 0, 'WIFEXITED($exitcode) after spawn '.$true );
+ is( ($exitcode >> 8), 0, 'WEXITSTATUS($exitcode) after spawn '.$true );
+ is( $dollarbang+0, 0, '$dollarbang after spawn '.$true );
+ is( $dollarat, '', '$dollarat after spawn '.$true );
+}
+
+# Just be paranoid in case anyone actually has this
+my $donotexist = "/bin/donotexist";
+$donotexist .= "X" while -e $donotexist;
+
+{
+ my ( $exited_pid, $exitcode, $dollarbang, $dollarat );
+ my $spawned_pid = $loop->spawn_child(
+ command => $donotexist,
+ on_exit => sub { ( $exited_pid, $exitcode, $dollarbang, $dollarat ) = @_; }
+ );
+
+ wait_for { defined $exited_pid };
+
+ is( $exited_pid, $spawned_pid, '$exited_pid == $spawned_pid after spawn donotexist' );
+ ok( ($exitcode & 0x7f) == 0, 'WIFEXITED($exitcode) after spawn donotexist' );
+ is( ($exitcode >> 8), 255, 'WEXITSTATUS($exitcode) after spawn donotexist' );
+ is( $dollarbang+0, ENOENT, '$dollarbang numerically after spawn donotexist' );
+ is( "$dollarbang", ENOENT_MESSAGE, '$dollarbang string after spawn donotexist' );
+ is( $dollarat, '', '$dollarat after spawn donotexist' );
+}
+
+{
+ my ( $exited_pid, $exitcode, $dollarbang, $dollarat );
+ my $spawned_pid = $loop->spawn_child(
+ command => [ $^X, "-e", "exit 14" ],
+ on_exit => sub { ( $exited_pid, $exitcode, $dollarbang, $dollarat ) = @_; }
+ );
+
+ wait_for { defined $exited_pid };
+
+ is( $exited_pid, $spawned_pid, '$exited_pid == $spawned_pid after spawn ARRAY' );
+ ok( ($exitcode & 0x7f) == 0, 'WIFEXITED($exitcode) after spawn ARRAY' );
+ is( ($exitcode >> 8), 14, 'WEXITSTATUS($exitcode) after spawn ARRAY' );
+ is( $dollarbang+0, 0, '$dollarbang after spawn ARRAY' );
+ is( $dollarat, '', '$dollarat after spawn ARRAY' );
+}
+
+{
+ my( $pipe_r, $pipe_w ) = IO::Async::OS->pipepair or die "Cannot pipepair - $!";
+
+ my ( $exited_pid, $exitcode, $dollarbang, $dollarat );
+ my $spawned_pid = $loop->spawn_child(
+ code => sub { return $pipe_w->syswrite( "test" ); },
+ on_exit => sub { ( $exited_pid, $exitcode, $dollarbang, $dollarat ) = @_; }
+ );
+
+ wait_for { defined $exited_pid };
+
+ is( $exited_pid, $spawned_pid, '$exited_pid == $spawned_pid after pipe close test' );
+ ok( ($exitcode & 0x7f) == 0, 'WIFEXITED($exitcode) after pipe close test' );
+ is( ($exitcode >> 8), 255, 'WEXITSTATUS($exitcode) after pipe close test' );
+ is( $dollarbang+0, EBADF, '$dollarbang numerically after pipe close test' );
+ is( $dollarat, '', '$dollarat after pipe close test' );
+}
+
+done_testing;
diff --git a/t/32loop-spawnchild-setup.t b/t/32loop-spawnchild-setup.t
new file mode 100644
index 0000000..7ecdf85
--- /dev/null
+++ b/t/32loop-spawnchild-setup.t
@@ -0,0 +1,439 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+use Test::Fatal;
+
+use File::Temp qw( tmpnam );
+use POSIX qw( ENOENT EBADF getcwd );
+
+use IO::Async::Loop;
+use IO::Async::OS;
+
+plan skip_all => "POSIX fork() is not available" unless IO::Async::OS->HAVE_POSIX_FORK;
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+ok( exception { $loop->spawn_child( code => sub { 1 }, setup => "hello" ); },
+ 'Bad setup type fails' );
+
+ok( exception { $loop->spawn_child( code => sub { 1 }, setup => [ 'somerandomthing' => 1 ] ); },
+ 'Setup with bad key fails' );
+
+# These tests are all very similar looking, with slightly different start and
+# code values. Easiest to wrap them up in a common testing wrapper.
+
+sub TEST
+{
+ my ( $name, %attr ) = @_;
+
+ my $exitcode;
+ my $dollarbang;
+ my $dollarat;
+
+ my ( undef, $callerfile, $callerline ) = caller;
+
+ $loop->spawn_child(
+ code => $attr{code},
+ exists $attr{setup} ? ( setup => $attr{setup} ) : (),
+ on_exit => sub { ( undef, $exitcode, $dollarbang, $dollarat ) = @_; },
+ );
+
+ wait_for { defined $exitcode };
+
+ if( exists $attr{exitstatus} ) {
+ ok( ($exitcode & 0x7f) == 0, "WIFEXITED(\$exitcode) after $name" );
+ is( ($exitcode >> 8), $attr{exitstatus}, "WEXITSTATUS(\$exitcode) after $name" );
+ }
+
+ if( exists $attr{dollarbang} ) {
+ is( $dollarbang+0, $attr{dollarbang}, "\$dollarbang numerically after $name" );
+ }
+
+ if( exists $attr{dollarat} ) {
+ is( $dollarat, $attr{dollarat}, "\$dollarat after $name" );
+ }
+}
+
+# A useful utility function like blocking read with a timeout
+sub read_timeout
+{
+ my ( $fh, undef, $len, $timeout ) = @_;
+
+ my $rvec = '';
+ vec( $rvec, fileno $fh, 1 ) = 1;
+
+ select( $rvec, undef, undef, $timeout );
+
+ return undef if !vec( $rvec, fileno $fh, 1 );
+
+ return $fh->read( $_[1], $len );
+}
+
+my $buffer;
+my $ret;
+
+{
+ my( $pipe_r, $pipe_w ) = IO::Async::OS->pipepair or die "Cannot pipepair - $!";
+
+ TEST "pipe dup to fd1",
+ setup => [ fd1 => [ 'dup', $pipe_w ] ],
+ code => sub { print "test"; },
+
+ exitstatus => 1,
+ dollarat => '';
+
+ undef $buffer;
+ $ret = read_timeout( $pipe_r, $buffer, 4, 0.1 );
+
+ is( $ret, 4, '$pipe_r->read after pipe dup to fd1' );
+ is( $buffer, 'test', '$buffer after pipe dup to fd1' );
+
+ my $pipe_w_fileno = fileno $pipe_w;
+
+ TEST "pipe dup to fd1 closes pipe",
+ setup => [ fd1 => [ 'dup', $pipe_w ] ],
+ code => sub {
+ my $f = IO::Handle->new_from_fd( $pipe_w_fileno, "w" );
+ defined $f and return 1;
+ $! == EBADF or return 1;
+ return 0;
+ },
+
+ exitstatus => 0,
+ dollarat => '';
+
+ TEST "pipe dup to stdout shortcut",
+ setup => [ stdout => $pipe_w ],
+ code => sub { print "test"; },
+
+ exitstatus => 1,
+ dollarat => '';
+
+ undef $buffer;
+ $ret = read_timeout( $pipe_r, $buffer, 4, 0.1 );
+
+ is( $ret, 4, '$pipe_r->read after pipe dup to stdout shortcut' );
+ is( $buffer, 'test', '$buffer after pipe dup to stdout shortcut' );
+
+ TEST "pipe dup to \\*STDOUT IO reference",
+ setup => [ \*STDOUT => $pipe_w ],
+ code => sub { print "test2"; },
+
+ exitstatus => 1,
+ dollarat => '';
+
+ undef $buffer;
+ $ret = read_timeout( $pipe_r, $buffer, 5, 0.1 );
+
+ is( $ret, 5, '$pipe_r->read after pipe dup to \\*STDOUT IO reference' );
+ is( $buffer, 'test2', '$buffer after pipe dup to \\*STDOUT IO reference' );
+
+ TEST "pipe keep open",
+ setup => [ "fd$pipe_w_fileno" => [ 'keep' ] ],
+ code => sub { $pipe_w->autoflush(1); $pipe_w->print( "test" ) },
+
+ exitstatus => 1,
+ dollarat => '';
+
+ undef $buffer;
+ $ret = read_timeout( $pipe_r, $buffer, 4, 0.1 );
+
+ is( $ret, 4, '$pipe_r->read after keep pipe open' );
+ is( $buffer, 'test', '$buffer after keep pipe open' );
+
+ TEST "pipe keep shortcut",
+ setup => [ "fd$pipe_w_fileno" => 'keep' ],
+ code => sub { $pipe_w->autoflush(1); $pipe_w->print( "test" ) },
+
+ exitstatus => 1,
+ dollarat => '';
+
+ undef $buffer;
+ $ret = read_timeout( $pipe_r, $buffer, 4, 0.1 );
+
+ is( $ret, 4, '$pipe_r->read after keep pipe open' );
+ is( $buffer, 'test', '$buffer after keep pipe open' );
+
+
+ TEST "pipe dup to stdout",
+ setup => [ stdout => [ 'dup', $pipe_w ] ],
+ code => sub { print "test"; },
+
+ exitstatus => 1,
+ dollarat => '';
+
+ undef $buffer;
+ $ret = read_timeout( $pipe_r, $buffer, 4, 0.1 );
+
+ is( $ret, 4, '$pipe_r->read after pipe dup to stdout' );
+ is( $buffer, 'test', '$buffer after pipe dup to stdout' );
+
+ TEST "pipe dup to fd2",
+ setup => [ fd2 => [ 'dup', $pipe_w ] ],
+ code => sub { print STDERR "test"; },
+
+ exitstatus => 1,
+ dollarat => '';
+
+ undef $buffer;
+ $ret = read_timeout( $pipe_r, $buffer, 4, 0.1 );
+
+ is( $ret, 4, '$pipe_r->read after pipe dup to fd2' );
+ is( $buffer, 'test', '$buffer after pipe dup to fd2' );
+
+ TEST "pipe dup to stderr",
+ setup => [ stderr => [ 'dup', $pipe_w ] ],
+ code => sub { print STDERR "test"; },
+
+ exitstatus => 1,
+ dollarat => '';
+
+ undef $buffer;
+ $ret = read_timeout( $pipe_r, $buffer, 4, 0.1 );
+
+ is( $ret, 4, '$pipe_r->read after pipe dup to stderr' );
+ is( $buffer, 'test', '$buffer after pipe dup to stderr' );
+
+ TEST "pipe dup to other FD",
+ setup => [ fd4 => [ 'dup', $pipe_w ] ],
+ code => sub {
+ close STDOUT;
+ open( STDOUT, ">&=4" ) or die "Cannot open fd4 as stdout - $!";
+ print "test";
+ },
+
+ exitstatus => 1,
+ dollarat => '';
+
+ undef $buffer;
+ $ret = read_timeout( $pipe_r, $buffer, 4, 0.1 );
+
+ is( $ret, 4, '$pipe_r->read after pipe dup to other FD' );
+ is( $buffer, 'test', '$buffer after pipe dup to other FD' );
+
+ TEST "pipe dup to its own FD",
+ setup => [ "fd$pipe_w_fileno" => $pipe_w ],
+ code => sub {
+ close STDOUT;
+ open( STDOUT, ">&=$pipe_w_fileno" ) or die "Cannot open fd$pipe_w_fileno as stdout - $!";
+ print "test";
+ },
+
+ exitstatus => 1,
+ dollarat => '';
+
+ undef $buffer;
+ $ret = read_timeout( $pipe_r, $buffer, 4, 0.1 );
+
+ is( $ret, 4, '$pipe_r->read after pipe dup to its own FD' );
+ is( $buffer, 'test', '$buffer after pipe dup to its own FD' );
+
+ TEST "other FD close",
+ code => sub { return $pipe_w->syswrite( "test" ); },
+
+ exitstatus => 255,
+ dollarbang => EBADF,
+ dollarat => '';
+
+ # Try to force a writepipe clash by asking to dup the pipe to lots of FDs
+ TEST "writepipe clash",
+ code => sub { print "test"; },
+ setup => [ map { +"fd$_" => $pipe_w } ( 1 .. 19 ) ],
+
+ exitstatus => 1,
+ dollarat => '';
+
+ undef $buffer;
+ $ret = read_timeout( $pipe_r, $buffer, 4, 0.1 );
+
+ is( $ret, 4, '$pipe_r->read after writepipe clash' );
+ is( $buffer, 'test', '$buffer after writepipe clash' );
+
+ my( $pipe2_r, $pipe2_w ) = IO::Async::OS->pipepair or die "Cannot pipepair - $!";
+ $pipe2_r->blocking( 0 );
+
+ TEST "pipe dup to stdout and stderr",
+ setup => [ stdout => $pipe_w, stderr => $pipe2_w ],
+ code => sub { print "output"; print STDERR "error"; },
+
+ exitstatus => 1,
+ dollarat => '';
+
+ undef $buffer;
+ $ret = read_timeout( $pipe_r, $buffer, 6, 0.1 );
+
+ is( $ret, 6, '$pipe_r->read after pipe dup to stdout and stderr' );
+ is( $buffer, 'output', '$buffer after pipe dup to stdout and stderr' );
+
+ undef $buffer;
+ $ret = read_timeout( $pipe2_r, $buffer, 5, 0.1 );
+
+ is( $ret, 5, '$pipe2_r->read after pipe dup to stdout and stderr' );
+ is( $buffer, 'error', '$buffer after pipe dup to stdout and stderr' );
+
+ TEST "pipe dup to stdout and stderr same pipe",
+ setup => [ stdout => $pipe_w, stderr => $pipe_w ],
+ code => sub { print "output"; print STDERR "error"; },
+
+ exitstatus => 1,
+ dollarat => '';
+
+ undef $buffer;
+ $ret = read_timeout( $pipe_r, $buffer, 11, 0.1 );
+
+ is( $ret, 11, '$pipe_r->read after pipe dup to stdout and stderr same pipe' );
+ is( $buffer, 'outputerror', '$buffer after pipe dup to stdout and stderr same pipe' );
+}
+
+{
+ my ( $child_r, $my_w, $my_r, $child_w ) = IO::Async::OS->pipequad or die "Cannot pipequad - $!";
+
+ $my_w->syswrite( "hello\n" );
+
+ TEST "pipe quad to fd0/fd1",
+ setup => [ stdin => $child_r,
+ stdout => $child_w, ],
+ code => sub { print uc scalar <STDIN>; return 0 },
+
+ exitstatus => 0,
+ dollarat => '';
+
+ my $buffer;
+ $ret = read_timeout( $my_r, $buffer, 6, 0.1 );
+
+ is( $ret, 6, '$my_r->read after pipe quad to fd0/fd1' );
+ is( $buffer, "HELLO\n", '$buffer after pipe quad to fd0/fd1' );
+}
+
+{
+ # Try to swap two filehandles and cause a dup2() collision
+ my @fhA = IO::Async::OS->pipepair or die "Cannot pipepair - $!";
+ my @fhB = IO::Async::OS->pipepair or die "Cannot pipepair - $!";
+
+ my $filenoA = $fhA[1]->fileno;
+ my $filenoB = $fhB[1]->fileno;
+
+ TEST "fd swap",
+ setup => [
+ "fd$filenoA" => $fhB[1],
+ "fd$filenoB" => $fhA[1],
+ ],
+ code => sub {
+ $fhA[1]->print( "FHA" ); $fhA[1]->autoflush(1);
+ $fhB[1]->print( "FHB" ); $fhB[1]->autoflush(1);
+ return 0;
+ },
+
+ exitstatus => 0;
+
+ my $buffer;
+
+ read_timeout( $fhA[0], $buffer, 3, 0.1 );
+ is( $buffer, "FHB", '$buffer [A] after dup2() swap' );
+
+ read_timeout( $fhB[0], $buffer, 3, 0.1 );
+ is( $buffer, "FHA", '$buffer [B] after dup2() swap' );
+}
+
+TEST "stdout close",
+ setup => [ stdout => [ 'close' ] ],
+ code => sub { print "test"; },
+
+ exitstatus => 255,
+ dollarbang => EBADF,
+ dollarat => '';
+
+TEST "stdout close shortcut",
+ setup => [ stdout => 'close' ],
+ code => sub { print "test"; },
+
+ exitstatus => 255,
+ dollarbang => EBADF,
+ dollarat => '';
+
+{
+ my $name = tmpnam;
+ END { unlink $name if defined $name and -f $name }
+
+ TEST "stdout open",
+ setup => [ stdout => [ 'open', '>', $name ] ],
+ code => sub { print "test"; },
+
+ exitstatus => 1,
+ dollarat => '';
+
+ ok( -f $name, 'tmpnam file exists after stdout open' );
+
+ open( my $tmpfh, "<", $name ) or die "Cannot open '$name' for reading - $!";
+
+ undef $buffer;
+ $ret = read_timeout( $tmpfh, $buffer, 4, 0.1 );
+
+ is( $ret, 4, '$tmpfh->read after stdout open' );
+ is( $buffer, 'test', '$buffer after stdout open' );
+
+ TEST "stdout open append",
+ setup => [ stdout => [ 'open', '>>', $name ] ],
+ code => sub { print "value"; },
+
+ exitstatus => 1,
+ dollarat => '';
+
+ seek( $tmpfh, 0, 0 );
+
+ undef $buffer;
+ $ret = read_timeout( $tmpfh, $buffer, 9, 0.1 );
+
+ is( $ret, 9, '$tmpfh->read after stdout open append' );
+ is( $buffer, 'testvalue', '$buffer after stdout open append' );
+}
+
+$ENV{TESTKEY} = "parent value";
+
+TEST "environment is preserved",
+ setup => [],
+ code => sub { return $ENV{TESTKEY} eq "parent value" ? 0 : 1 },
+
+ exitstatus => 0,
+ dollarat => '';
+
+TEST "environment is overwritten",
+ setup => [ env => { TESTKEY => "child value" } ],
+ code => sub { return $ENV{TESTKEY} eq "child value" ? 0 : 1 },
+
+ exitstatus => 0,
+ dollarat => '';
+
+SKIP: {
+ # Some of the CPAN smoke testers might run test scripts under modified nice
+ # anyway. We'd better get our starting value to check for difference, not
+ # absolute
+ my $prio_now = getpriority(0,0);
+
+ # If it's already quite high, we don't want to hit the limit and be
+ # clamped. Just skip the tests if it's too high before we start.
+ skip "getpriority is already above 15, so I won't try renicing upwards", 3 if $prio_now > 15;
+
+ TEST "nice works",
+ setup => [ nice => 3 ],
+ code => sub { return getpriority(0,0) == $prio_now + 3 ? 0 : 1 },
+
+ exitstatus => 0,
+ dollarat => '';
+}
+
+TEST "chdir works",
+ setup => [ chdir => "/" ],
+ code => sub { return getcwd eq "/" ? 0 : 1 },
+
+ exitstatus => 0,
+ dollarat => '';
+
+done_testing;
diff --git a/t/33process.t b/t/33process.t
new file mode 100644
index 0000000..72c07b1
--- /dev/null
+++ b/t/33process.t
@@ -0,0 +1,245 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+use Test::Refcount;
+
+use POSIX qw( ENOENT SIGTERM SIGUSR1 );
+use constant ENOENT_MESSAGE => do { local $! = ENOENT; "$!" };
+
+use IO::Async::Process;
+
+use IO::Async::Loop;
+use IO::Async::OS;
+
+plan skip_all => "POSIX fork() is not available" unless IO::Async::OS->HAVE_POSIX_FORK;
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+{
+ my ( $invocant, $exitcode );
+
+ my $process = IO::Async::Process->new(
+ code => sub { return 0 },
+ on_finish => sub { ( $invocant, $exitcode ) = @_; },
+ );
+
+ is_oneref( $process, '$process has refcount 1 before $loop->add' );
+
+ is( $process->notifier_name, "nopid", '$process->notifier_name before $loop->add' );
+
+ ok( !$process->is_running, '$process is not yet running' );
+ ok( !defined $process->pid, '$process has no PID yet' );
+
+ $loop->add( $process );
+
+ is_refcount( $process, 2, '$process has refcount 2 after $loop->add' );
+
+ my $pid = $process->pid;
+
+ ok( $process->is_running, '$process is running' );
+ ok( defined $pid, '$process now has a PID' );
+
+ is( $process->notifier_name, "$pid", '$process->notifier_name after $loop->add' );
+
+ wait_for { defined $exitcode };
+
+ is( $invocant, $process, '$_[0] in on_finish is $process' );
+ undef $invocant; # refcount
+
+ ok( ($exitcode & 0x7f) == 0, 'WIFEXITED($exitcode) after sub { 0 }' );
+ is( ($exitcode >> 8), 0, 'WEXITSTATUS($exitcode) after sub { 0 }' );
+
+ ok( !$process->is_running, '$process no longer running' );
+ ok( defined $process->pid, '$process still has PID after exit' );
+
+ is( $process->notifier_name, "[$pid]", '$process->notifier_name after exit' );
+
+ ok( $process->is_exited, '$process->is_exited after sub { 0 }' );
+ is( $process->exitstatus, 0, '$process->exitstatus after sub { 0 }' );
+
+ ok( !defined $process->loop, '$process no longer in Loop' );
+
+ is_oneref( $process, '$process has refcount 1 before EOS' );
+}
+
+{
+ my $process = IO::Async::Process->new(
+ code => sub { return 3 },
+ on_finish => sub { },
+ );
+
+ $loop->add( $process );
+
+ wait_for { !$process->is_running };
+
+ ok( $process->is_exited, '$process->is_exited after sub { 3 }' );
+ is( $process->exitstatus, 3, '$process->exitstatus after sub { 3 }' );
+}
+
+{
+ my ( $invocant, $exception, $exitcode );
+
+ my $process = IO::Async::Process->new(
+ code => sub { die "An exception\n" },
+ on_finish => sub { die "Test failed early\n" },
+ on_exception => sub { ( $invocant, $exception, undef, $exitcode ) = @_ },
+ );
+
+ is_oneref( $process, '$process has refcount 1 before $loop->add' );
+
+ $loop->add( $process );
+
+ is_refcount( $process, 2, '$process has refcount 2 after $loop->add' );
+
+ wait_for { defined $exitcode };
+
+ is( $invocant, $process, '$_[0] in on_exception is $process' );
+ undef $invocant; # refcount
+
+ ok( ($exitcode & 0x7f) == 0, 'WIFEXITED($exitcode) after sub { die }' );
+ is( ($exitcode >> 8), 255, 'WEXITSTATUS($exitcode) after sub { die }' );
+ is( $exception, "An exception\n", '$exception after sub { die }' );
+
+ ok( $process->is_exited, '$process->is_exited after sub { die }' );
+ is( $process->exitstatus, 255, '$process->exitstatus after sub { die }' );
+ is( $process->exception, "An exception\n", '$process->exception after sub { die }' );
+
+ is_oneref( $process, '$process has refcount 1 before EOS' );
+}
+
+{
+ my $exitcode;
+
+ my $process = IO::Async::Process->new(
+ code => sub { die "An exception\n" },
+ on_finish => sub { ( undef, $exitcode ) = @_ },
+ );
+
+ $loop->add( $process );
+
+ wait_for { defined $exitcode };
+
+ ok( ($exitcode & 0x7f) == 0, 'WIFEXITED($exitcode) after sub { die } on_finish' );
+ is( ($exitcode >> 8), 255, 'WEXITSTATUS($exitcode) after sub { die } on_finish' );
+
+ ok( $process->is_exited, '$process->is_exited after sub { die } on_finish' );
+ is( $process->exitstatus, 255, '$process->exitstatus after sub { die } on_finish' );
+ is( $process->exception, "An exception\n", '$process->exception after sub { die } on_finish' );
+}
+
+{
+ my $process = IO::Async::Process->new(
+ command => [ $^X, "-e", '1' ],
+ on_finish => sub { },
+ );
+
+ $loop->add( $process );
+
+ wait_for { !$process->is_running };
+
+ ok( $process->is_exited, '$process->is_exited after perl -e 1' );
+ is( $process->exitstatus, 0, '$process->exitstatus after perl -e 1' );
+}
+
+{
+ my $process = IO::Async::Process->new(
+ command => [ $^X, "-e", 'exit 5' ],
+ on_finish => sub { },
+ );
+
+ $loop->add( $process );
+
+ wait_for { !$process->is_running };
+
+ ok( $process->is_exited, '$process->is_exited after perl -e exit 5' );
+ is( $process->exitstatus, 5, '$process->exitstatus after perl -e exit 5' );
+}
+
+{
+ # Just be paranoid in case anyone actually has this
+ my $donotexist = "/bin/donotexist";
+ $donotexist .= "X" while -e $donotexist;
+
+ my ( $exception, $errno );
+
+ my $process = IO::Async::Process->new(
+ command => $donotexist,
+ on_finish => sub { die "Test failed early\n" },
+ on_exception => sub { ( undef, $exception, $errno ) = @_ },
+ );
+
+ $loop->add( $process );
+
+ wait_for { !$process->is_running };
+
+ is( $errno+0, ENOENT, '$errno number after donotexist' );
+ is( "$errno", ENOENT_MESSAGE, '$errno string after donotexist' );
+
+ ok( $process->is_exited, '$process->is_exited after donotexist' );
+ is( $process->exitstatus, 255, '$process->exitstatus after donotexist' );
+ is( $process->errno, ENOENT, '$process->errno number after donotexist' );
+ is( $process->errstr, ENOENT_MESSAGE, '$process->errno string after donotexist' );
+ is( $process->exception, "", '$process->exception after donotexist' );
+}
+
+{
+ $ENV{TEST_KEY} = "foo";
+
+ my $process = IO::Async::Process->new(
+ code => sub { $ENV{TEST_KEY} eq "bar" ? 0 : 1 },
+ setup => [
+ env => { TEST_KEY => "bar" },
+ ],
+ on_finish => sub { },
+ );
+
+ $loop->add( $process );
+
+ wait_for { !$process->is_running };
+
+ ok( $process->is_exited, '$process->is_exited after %ENV test' );
+ is( $process->exitstatus, 0, '$process->exitstatus after %ENV test' );
+}
+
+SKIP: {
+ skip "This OS does not have signals", 2 unless IO::Async::OS->HAVE_SIGNALS;
+
+ my $child_ready;
+ $loop->watch_signal( USR1 => sub { $child_ready++ } );
+
+ my $parentpid = $$;
+ my $process = IO::Async::Process->new(
+ code => sub {
+ my $exitcode = 10;
+ eval {
+ local $SIG{TERM} = sub { $exitcode = 20; die };
+ kill SIGUSR1 => $parentpid;
+ sleep 60; # block on signal
+ };
+ return $exitcode;
+ },
+ on_finish => sub { },
+ );
+
+ $loop->add( $process );
+
+ wait_for { $child_ready };
+
+ $process->kill( SIGTERM );
+
+ wait_for { !$process->is_running };
+
+ ok( $process->is_exited, '$process->is_exited after ->kill' );
+ is( $process->exitstatus, 20, '$process->exitstatus after ->kill' );
+
+ $loop->unwatch_signal( USR1 => );
+}
+
+done_testing;
diff --git a/t/34process-handles.t b/t/34process-handles.t
new file mode 100644
index 0000000..f199ef7
--- /dev/null
+++ b/t/34process-handles.t
@@ -0,0 +1,429 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+
+use IO::Async::Process;
+
+use IO::Async::Loop;
+use IO::Async::OS;
+
+plan skip_all => "POSIX fork() is not available" unless IO::Async::OS->HAVE_POSIX_FORK;
+
+use Socket qw( PF_INET sockaddr_family );
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+{
+ my $process = IO::Async::Process->new(
+ code => sub { print "hello\n"; return 0 },
+ stdout => { via => "pipe_read" },
+ on_finish => sub { },
+ );
+
+ isa_ok( $process->stdout, "IO::Async::Stream", '$process->stdout' );
+
+ is( $process->stdout->notifier_name, "stdout", '$process->stdout->notifier_name' );
+
+ my @stdout_lines;
+
+ $process->stdout->configure(
+ on_read => sub {
+ my ( undef, $buffref ) = @_;
+ push @stdout_lines, $1 while $$buffref =~ s/^(.*\n)//;
+ return 0;
+ },
+ );
+
+ $loop->add( $process );
+
+ ok( defined $process->stdout->read_handle, '$process->stdout has read_handle for sub { print }' );
+
+ wait_for { !$process->is_running };
+
+ ok( $process->is_exited, '$process->is_exited after sub { print }' );
+ is( $process->exitstatus, 0, '$process->exitstatus after sub { print }' );
+
+ is_deeply( \@stdout_lines, [ "hello\n" ], '@stdout_lines after sub { print }' );
+}
+
+{
+ my @stdout_lines;
+
+ my $process = IO::Async::Process->new(
+ code => sub { print "hello\n"; return 0 },
+ stdout => {
+ on_read => sub {
+ my ( undef, $buffref ) = @_;
+ push @stdout_lines, $1 while $$buffref =~ s/^(.*\n)//;
+ return 0;
+ },
+ },
+ on_finish => sub { },
+ );
+
+ isa_ok( $process->stdout, "IO::Async::Stream", '$process->stdout' );
+
+ $loop->add( $process );
+
+ ok( defined $process->stdout->read_handle, '$process->stdout has read_handle for sub { print } inline' );
+
+ wait_for { !$process->is_running };
+
+ ok( $process->is_exited, '$process->is_exited after sub { print } inline' );
+ is( $process->exitstatus, 0, '$process->exitstatus after sub { print } inline' );
+
+ is_deeply( \@stdout_lines, [ "hello\n" ], '@stdout_lines after sub { print } inline' );
+}
+
+{
+ my $stdout;
+
+ my $process = IO::Async::Process->new(
+ code => sub { print "hello\n"; return 0 },
+ stdout => { into => \$stdout },
+ on_finish => sub { },
+ );
+
+ isa_ok( $process->stdout, "IO::Async::Stream", '$process->stdout' );
+
+ $loop->add( $process );
+
+ ok( defined $process->stdout->read_handle, '$process->stdout has read_handle for sub { print } into' );
+
+ wait_for { !$process->is_running };
+
+ ok( $process->is_exited, '$process->is_exited after sub { print } into' );
+ is( $process->exitstatus, 0, '$process->exitstatus after sub { print } into' );
+
+ is( $stdout, "hello\n", '$stdout after sub { print } into' )
+}
+
+{
+ my $stdout;
+
+ my $process = IO::Async::Process->new(
+ command => [ $^X, "-e", 'print "hello\n"' ],
+ stdout => { into => \$stdout },
+ on_finish => sub { },
+ );
+
+ $loop->add( $process );
+
+ wait_for { !$process->is_running };
+
+ ok( $process->is_exited, '$process->is_exited after perl STDOUT' );
+ is( $process->exitstatus, 0, '$process->exitstatus after perl STDOUT' );
+
+ is( $stdout, "hello\n", '$stdout after perl STDOUT' );
+}
+
+{
+ my $stdout;
+ my $stderr;
+
+ my $process = IO::Async::Process->new(
+ command => [ $^X, "-e", 'print STDOUT "output\n"; print STDERR "error\n";' ],
+ stdout => { into => \$stdout },
+ stderr => { into => \$stderr },
+ on_finish => sub { },
+ );
+
+ isa_ok( $process->stderr, "IO::Async::Stream", '$process->stderr' );
+
+ is( $process->stderr->notifier_name, "stderr", '$process->stderr->notifier_name' );
+
+ $loop->add( $process );
+
+ ok( defined $process->stderr->read_handle, '$process->stderr has read_handle' );
+
+ wait_for { !$process->is_running };
+
+ ok( $process->is_exited, '$process->is_exited after perl STDOUT/STDERR' );
+ is( $process->exitstatus, 0, '$process->exitstatus after perl STDOUT/STDERR' );
+
+ is( $stdout, "output\n", '$stdout after perl STDOUT/STDERR' );
+ is( $stderr, "error\n", '$stderr after perl STDOUT/STDERR' );
+}
+
+{
+ my $stdout;
+
+ my $process = IO::Async::Process->new(
+ command => [ $^X, "-pe", '$_ = uc' ],
+ stdin => { via => "pipe_write" },
+ stdout => { into => \$stdout },
+ on_finish => sub { },
+ );
+
+ isa_ok( $process->stdin, "IO::Async::Stream", '$process->stdin' );
+
+ is( $process->stdin->notifier_name, "stdin", '$process->stdin->notifier_name' );
+
+ $process->stdin->write( "some data\n", on_flush => sub { $_[0]->close } );
+
+ $loop->add( $process );
+
+ ok( defined $process->stdin->write_handle, '$process->stdin has write_handle for perl STDIN->STDOUT' );
+
+ wait_for { !$process->is_running };
+
+ ok( $process->is_exited, '$process->is_exited after perl STDIN->STDOUT' );
+ is( $process->exitstatus, 0, '$process->exitstatus after perl STDIN->STDOUT' );
+
+ is( $stdout, "SOME DATA\n", '$stdout after perl STDIN->STDOUT' );
+}
+
+{
+ my $process = IO::Async::Process->new(
+ command => [ $^X, "-e", 'exit 4' ],
+ stdin => { via => "pipe_write" },
+ on_finish => sub { },
+ );
+
+ isa_ok( $process->stdin, "IO::Async::Stream", '$process->stdin' );
+
+ $loop->add( $process );
+
+ ok( defined $process->stdin->write_handle, '$process->stdin has write_handle for perl STDIN no-wait close' );
+
+ wait_for { !$process->is_running };
+
+ ok( $process->is_exited, '$process->is_exited after perl STDIN no-wait close' );
+ is( $process->exitstatus, 4, '$process->exitstatus after perl STDIN no-wait close' );
+}
+
+{
+ my $stdout;
+
+ my $process = IO::Async::Process->new(
+ command => [ $^X, "-pe", '$_ = uc' ],
+ stdin => { from => "some data\n" },
+ stdout => { into => \$stdout },
+ on_finish => sub { },
+ );
+
+ isa_ok( $process->stdin, "IO::Async::Stream", '$process->stdin' );
+
+ $loop->add( $process );
+
+ ok( defined $process->stdin->write_handle, '$process->stdin has write_handle for perl STDIN->STDOUT from' );
+
+ wait_for { !$process->is_running };
+
+ ok( $process->is_exited, '$process->is_exited after perl STDIN->STDOUT from' );
+ is( $process->exitstatus, 0, '$process->exitstatus after perl STDIN->STDOUT from' );
+
+ is( $stdout, "SOME DATA\n", '$stdout after perl STDIN->STDOUT from' );
+}
+
+{
+ my $stdout;
+
+ my $process = IO::Async::Process->new(
+ command => [ $^X, "-pe", '$_ = "line"' ],
+ stdin => { from => "" },
+ stdout => { into => \$stdout },
+ on_finish => sub { },
+ );
+
+ isa_ok( $process->stdin, "IO::Async::Stream", '$process->stdin' );
+
+ $loop->add( $process );
+
+ ok( defined $process->stdin->write_handle, '$process->stdin has write_handle for perl STDIN->STDOUT from empty string' );
+
+ wait_for { !$process->is_running };
+
+ ok( $process->is_exited, '$process->is_exited after perl STDIN->STDOUT from empty string' );
+ is( $process->exitstatus, 0, '$process->exitstatus after perl STDIN->STDOUT from empty string' );
+
+ is( $stdout, "", '$stdout after perl STDIN->STDOUT from empty string' );
+}
+
+{
+ my $stdout;
+
+ my $process = IO::Async::Process->new(
+ command => [ $^X, "-pe", '$_ = uc' ],
+ fd0 => { from => "some data\n" },
+ fd1 => { into => \$stdout },
+ on_finish => sub { },
+ );
+
+ $loop->add( $process );
+
+ wait_for { !$process->is_running };
+
+ ok( $process->is_exited, '$process->is_exited after perl STDIN->STDOUT using fd[n]' );
+ is( $process->exitstatus, 0, '$process->exitstatus after perl STDIN->STDOUT using fd[n]' );
+
+ is( $stdout, "SOME DATA\n", '$stdout after perl STDIN->STDOUT using fd[n]' );
+}
+
+{
+ my $output;
+
+ my $process = IO::Async::Process->new(
+ command => [ $^X, "-pe", '$_ = uc' ],
+ stdio => { via => "pipe_rdwr" },
+ on_finish => sub { },
+ );
+
+ isa_ok( $process->stdio, "IO::Async::Stream", '$process->stdio' );
+
+ is( $process->stdio->notifier_name, "stdio", '$process->stdio->notifier_name' );
+
+ my @output_lines;
+
+ $process->stdio->write( "some data\n", on_flush => sub { $_[0]->close_write } );
+ $process->stdio->configure(
+ on_read => sub {
+ my ( undef, $buffref ) = @_;
+ push @output_lines, $1 while $$buffref =~ s/^(.*\n)//;
+ return 0;
+ },
+ );
+
+ $loop->add( $process );
+
+ ok( defined $process->stdio->read_handle, '$process->stdio has read_handle for perl STDIO' );
+ ok( defined $process->stdio->write_handle, '$process->stdio has write_handle for perl STDIO' );
+
+ wait_for { !$process->is_running };
+
+ ok( $process->is_exited, '$process->is_exited after perl STDIO' );
+ is( $process->exitstatus, 0, '$process->exitstatus after perl STDIO' );
+
+ is_deeply( \@output_lines, [ "SOME DATA\n" ], '@output_lines after perl STDIO' );
+}
+
+{
+ my $output;
+
+ my $process = IO::Async::Process->new(
+ command => [ $^X, "-pe", '$_ = uc' ],
+ stdio => {
+ from => "some data\n",
+ into => \$output,
+ },
+ on_finish => sub { },
+ );
+
+ $loop->add( $process );
+
+ wait_for { !$process->is_running };
+
+ ok( $process->is_exited, '$process->is_exited after perl STDIN->STDOUT using stdio' );
+ is( $process->exitstatus, 0, '$process->exitstatus after perl STDIN->STDOUT using stdio' );
+
+ is( $output, "SOME DATA\n", '$stdout after perl STDIN->STDOUT using stdio' );
+}
+
+{
+ my $process = IO::Async::Process->new(
+ code => sub {
+ defined( recv STDIN, my $pkt, 8192, 0 ) or die "Cannot recv - $!";
+ send STDOUT, $pkt, 0 or die "Cannot send - $!";
+ return 0;
+ },
+ stdio => { via => "socketpair" },
+ on_finish => sub { },
+ );
+
+ isa_ok( $process->stdio, "IO::Async::Stream", '$process->stdio isa Stream' );
+
+ $process->stdio->write( "A packet to be echoed" );
+
+ my $output_packet = "";
+ $process->stdio->configure(
+ on_read => sub {
+ my ( undef, $buffref ) = @_;
+ $output_packet .= $$buffref;
+ $$buffref = "";
+ return 0;
+ },
+ );
+
+ $loop->add( $process );
+
+ isa_ok( $process->stdio->read_handle, "IO::Socket", '$process->stdio handle isa IO::Socket' );
+
+ wait_for { defined $output_packet and !$process->is_running };
+
+ ok( $process->is_exited, '$process->is_exited after perl STDIO via socketpair' );
+ is( $process->exitstatus, 0, '$process->exitstatus after perl STDIO via socketpair' );
+
+ is_deeply( $output_packet, "A packet to be echoed", '$output_packet after perl STDIO via socketpair' );
+}
+
+{
+ my $process = IO::Async::Process->new(
+ code => sub { return 0 },
+ stdio => { via => "socketpair", family => "inet" },
+ on_finish => sub { },
+ );
+
+ isa_ok( $process->stdio, "IO::Async::Stream", '$process->stdio isa Stream' );
+
+ $process->stdio->configure( on_read => sub { } );
+
+ $loop->add( $process );
+
+ isa_ok( $process->stdio->read_handle, "IO::Socket", '$process->stdio handle isa IO::Socket' );
+ is( sockaddr_family( $process->stdio->read_handle->sockname ), PF_INET, '$process->stdio handle sockdomain is PF_INET' );
+
+ wait_for { !$process->is_running };
+}
+
+{
+ my $process = IO::Async::Process->new(
+ code => sub {
+ for( 1, 2 ) {
+ defined( recv STDIN, my $pkt, 8192, 0 ) or die "Cannot recv - $!";
+ send STDOUT, $pkt, 0 or die "Cannot send - $!";
+ }
+ return 0;
+ },
+ stdio => { via => "socketpair", socktype => "dgram", family => "inet" },
+ on_finish => sub { },
+ );
+
+ isa_ok( $process->stdio, "IO::Async::Socket", '$process->stdio isa Socket' );
+
+ my @output_packets;
+ $process->stdio->configure(
+ on_recv => sub {
+ my ( $self, $packet ) = @_;
+ push @output_packets, $packet;
+
+ $self->close if @output_packets == 2;
+
+ return 0;
+ },
+ );
+
+ $loop->add( $process );
+
+ isa_ok( $process->stdio->read_handle, "IO::Socket", '$process->stdio handle isa IO::Socket' );
+ ok( defined sockaddr_family( $process->stdio->read_handle->sockname ), '$process->stdio handle sockdomain is defined' );
+
+ $process->stdio->send( $_ ) for "First packet", "Second packet";
+
+ wait_for { @output_packets == 2 and !$process->is_running };
+
+ ok( $process->is_exited, '$process->is_exited after perl STDIO via dgram socketpair' );
+ is( $process->exitstatus, 0, '$process->exitstatus after perl STDIO via dgram socketpair' );
+
+ is_deeply( \@output_packets,
+ [ "First packet", "Second packet" ],
+ '@output_packets after perl STDIO via dgram socketpair' );
+}
+
+done_testing;
diff --git a/t/35loop-openchild.t b/t/35loop-openchild.t
new file mode 100644
index 0000000..608952d
--- /dev/null
+++ b/t/35loop-openchild.t
@@ -0,0 +1,65 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+use Test::Fatal;
+
+use IO::Async::Loop;
+use IO::Async::OS;
+
+plan skip_all => "POSIX fork() is not available" unless IO::Async::OS->HAVE_POSIX_FORK;
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+my $exitcode;
+
+$loop->open_child(
+ code => sub { 0 },
+ on_finish => sub { ( undef, $exitcode ) = @_; },
+);
+
+undef $exitcode;
+wait_for { defined $exitcode };
+
+ok( ($exitcode & 0x7f) == 0, 'WIFEXITED($exitcode) after sub { 0 }' );
+is( ($exitcode >> 8), 0, 'WEXITSTATUS($exitcode) after sub { 0 }' );
+
+$loop->open_child(
+ command => [ $^X, "-e", 'exit 5' ],
+ on_finish => sub { ( undef, $exitcode ) = @_; },
+);
+
+undef $exitcode;
+wait_for { defined $exitcode };
+
+ok( ($exitcode & 0x7f) == 0, 'WIFEXITED($exitcode) after perl -e exit 5' );
+is( ($exitcode >> 8), 5, 'WEXITSTATUS($exitcode) after perl -e exit 5' );
+
+ok( exception { $loop->open_child(
+ command => [ $^X, "-e", 1 ]
+ ) },
+ 'Missing on_finish fails'
+);
+
+ok( exception { $loop->open_child(
+ command => [ $^X, "-e", 1 ],
+ on_finish => "hello"
+ ) },
+ 'on_finish not CODE ref fails'
+);
+
+ok( exception { $loop->open_child(
+ command => [ $^X, "-e", 1 ],
+ on_finish => sub {},
+ on_exit => sub {},
+ ) },
+ 'on_exit parameter fails'
+);
+
+done_testing;
diff --git a/t/36loop-runchild.t b/t/36loop-runchild.t
new file mode 100644
index 0000000..50801d1
--- /dev/null
+++ b/t/36loop-runchild.t
@@ -0,0 +1,158 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+use Test::Fatal;
+
+use IO::Async::Loop;
+use IO::Async::OS;
+
+plan skip_all => "POSIX fork() is not available" unless IO::Async::OS->HAVE_POSIX_FORK;
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+my ( $exitcode, $child_out, $child_err );
+
+$loop->run_child(
+ code => sub { 0 },
+ on_finish => sub { ( undef, $exitcode, $child_out, $child_err ) = @_; },
+);
+
+undef $exitcode;
+wait_for { defined $exitcode };
+
+ok( ($exitcode & 0x7f) == 0, 'WIFEXITED($exitcode) after sub { 0 }' );
+is( ($exitcode >> 8), 0, 'WEXITSTATUS($exitcode) after sub { 0 }' );
+is( $child_out, "", '$child_out after sub { 0 }' );
+is( $child_err, "", '$child_err after sub { 0 }' );
+
+$loop->run_child(
+ code => sub { 3 },
+ on_finish => sub { ( undef, $exitcode, $child_out, $child_err ) = @_; },
+);
+
+undef $exitcode;
+wait_for { defined $exitcode };
+
+ok( ($exitcode & 0x7f) == 0, 'WIFEXITED($exitcode) after sub { 3 }' );
+is( ($exitcode >> 8), 3, 'WEXITSTATUS($exitcode) after sub { 3 }' );
+is( $child_out, "", '$child_out after sub { 3 }' );
+is( $child_err, "", '$child_err after sub { 3 }' );
+
+$loop->run_child(
+ command => [ $^X, "-e", '1' ],
+ on_finish => sub { ( undef, $exitcode, $child_out, $child_err ) = @_; },
+);
+
+undef $exitcode;
+wait_for { defined $exitcode };
+
+ok( ($exitcode & 0x7f) == 0, 'WIFEXITED($exitcode) after perl -e 1' );
+is( ($exitcode >> 8), 0, 'WEXITSTATUS($exitcode) after perl -e 1' );
+is( $child_out, "", '$child_out after perl -e 1' );
+is( $child_err, "", '$child_err after perl -e 1' );
+
+$loop->run_child(
+ command => [ $^X, "-e", 'exit 5' ],
+ on_finish => sub { ( undef, $exitcode, $child_out, $child_err ) = @_; },
+);
+
+undef $exitcode;
+wait_for { defined $exitcode };
+
+ok( ($exitcode & 0x7f) == 0, 'WIFEXITED($exitcode) after perl -e exit 5' );
+is( ($exitcode >> 8), 5, 'WEXITSTATUS($exitcode) after perl -e exit 5' );
+is( $child_out, "", '$child_out after perl -e exit 5' );
+is( $child_err, "", '$child_err after perl -e exit 5' );
+
+$loop->run_child(
+ code => sub { print "hello\n"; 0 },
+ on_finish => sub { ( undef, $exitcode, $child_out, $child_err ) = @_; },
+);
+
+undef $exitcode;
+wait_for { defined $exitcode };
+
+ok( ($exitcode & 0x7f) == 0, 'WIFEXITED($exitcode) after sub { print }' );
+is( ($exitcode >> 8), 0, 'WEXITSTATUS($exitcode) after sub { print }' );
+is( $child_out, "hello\n", '$child_out after sub { print }' );
+is( $child_err, "", '$child_err after sub { print }' );
+
+$loop->run_child(
+ command => [ $^X, "-e", 'print "goodbye\n"' ],
+ on_finish => sub { ( undef, $exitcode, $child_out, $child_err ) = @_; },
+);
+
+undef $exitcode;
+wait_for { defined $exitcode };
+
+ok( ($exitcode & 0x7f) == 0, 'WIFEXITED($exitcode) after perl STDOUT' );
+is( ($exitcode >> 8), 0, 'WEXITSTATUS($exitcode) after perl STDOUT' );
+is( $child_out, "goodbye\n", '$child_out after perl STDOUT' );
+is( $child_err, "", '$child_err after perl STDOUT' );
+
+$loop->run_child(
+ command => [ $^X, "-e", 'print STDOUT "output\n"; print STDERR "error\n";' ],
+ on_finish => sub { ( undef, $exitcode, $child_out, $child_err ) = @_; },
+);
+
+undef $exitcode;
+wait_for { defined $exitcode };
+
+ok( ($exitcode & 0x7f) == 0, 'WIFEXITED($exitcode) after perl STDOUT/STDERR' );
+is( ($exitcode >> 8), 0, 'WEXITSTATUS($exitcode) after perl STDOUT/STDERR' );
+is( $child_out, "output\n", '$child_out after perl STDOUT/STDERR' );
+is( $child_err, "error\n", '$child_err after perl STDOUT/STDERR' );
+
+# perl -pe 1 behaves like cat; copies STDIN to STDOUT
+
+$loop->run_child(
+ command => [ $^X, "-pe", '1' ],
+ stdin => "some data\n",
+ on_finish => sub { ( undef, $exitcode, $child_out, $child_err ) = @_; },
+);
+
+undef $exitcode;
+wait_for { defined $exitcode };
+
+ok( ($exitcode & 0x7f) == 0, 'WIFEXITED($exitcode) after perl STDIN->STDOUT' );
+is( ($exitcode >> 8), 0, 'WEXITSTATUS($exitcode) after perl STDIN->STDOUT' );
+is( $child_out, "some data\n", '$child_out after perl STDIN->STDOUT' );
+is( $child_err, "", '$child_err after perl STDIN->STDOUT' );
+
+ok( exception { $loop->run_child(
+ command => [ $^X, "-e", 1 ]
+ ) },
+ 'Missing on_finish fails'
+);
+
+ok( exception { $loop->run_child(
+ command => [ $^X, "-e", 1 ],
+ on_finish => "hello"
+ ) },
+ 'on_finish not CODE ref fails'
+);
+
+ok( exception { $loop->run_child(
+ command => [ $^X, "-e", 1 ],
+ on_finish => sub {},
+ on_exit => sub {},
+ ) },
+ 'on_exit parameter fails'
+);
+
+ok( exception { $loop->run_child(
+ command => [ $^X, "-e", 1 ],
+ on_finish => sub {},
+ some_key_you_fail => 1
+ ) },
+ 'unrecognised key fails'
+);
+
+done_testing;
diff --git a/t/37loop-child-root.t b/t/37loop-child-root.t
new file mode 100644
index 0000000..237b338
--- /dev/null
+++ b/t/37loop-child-root.t
@@ -0,0 +1,89 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+
+use IO::Async::Loop;
+use IO::Async::OS;
+
+plan skip_all => "POSIX fork() is not available" unless IO::Async::OS->HAVE_POSIX_FORK;
+
+use POSIX qw( WEXITSTATUS );
+
+# These tests check the parts of Loop->spawn_child that need to be root to
+# work. Since we're unlikely to be root, skip the lot if we're not.
+
+unless( $< == 0 ) {
+ plan skip_all => "not root";
+}
+
+is( $>, 0, 'am root');
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+my ( $exitcode, $dollarbang, $dollarat );
+
+$loop->spawn_child(
+ code => sub { return $> },
+ setup => [ setuid => 10 ],
+ on_exit => sub { ( undef, $exitcode, $dollarbang, $dollarat ) = @_ },
+);
+
+wait_for { defined $exitcode };
+
+is( WEXITSTATUS($exitcode), 10, 'setuid' );
+
+$loop->spawn_child(
+ code => sub { return $) },
+ setup => [ setgid => 10 ],
+ on_exit => sub { ( undef, $exitcode, $dollarbang, $dollarat ) = @_ },
+);
+
+undef $exitcode;
+wait_for { defined $exitcode };
+
+is( WEXITSTATUS($exitcode), 10, 'setgid' );
+
+$loop->spawn_child(
+ code => sub { return $) =~ m/ 5 / },
+ setup => [ setgroups => [ 4, 5, 6 ] ],
+ on_exit => sub { ( undef, $exitcode, $dollarbang, $dollarat ) = @_ },
+);
+
+undef $exitcode;
+wait_for { defined $exitcode };
+
+is( WEXITSTATUS($exitcode), 1, 'setgroups' );
+
+my $child_out;
+
+$loop->run_child(
+ code => sub {
+ print "EUID: $>\n";
+ my ( $gid, @groups ) = split( m/ /, $) );
+ print "EGID: $gid\n";
+ print "Groups: " . join( " ", sort { $a <=> $b } @groups ) . "\n";
+ return 0;
+ },
+ setup => [
+ setgid => 10,
+ setgroups => [ 4, 5, 6, 10 ],
+ setuid => 20,
+ ],
+ on_finish => sub { ( undef, $exitcode, $child_out ) = @_; },
+);
+
+undef $exitcode;
+wait_for { defined $exitcode };
+
+is( $child_out,
+ "EUID: 20\nEGID: 10\nGroups: 4 5 6 10\n",
+ 'combined setuid/gid/groups' );
+
+done_testing;
diff --git a/t/38loop-thread.t b/t/38loop-thread.t
new file mode 100644
index 0000000..f2d2389
--- /dev/null
+++ b/t/38loop-thread.t
@@ -0,0 +1,59 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+
+use IO::Async::Loop;
+use IO::Async::OS;
+
+plan skip_all => "Threads are not available" unless IO::Async::OS->HAVE_THREADS;
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+# thread in scalar context
+{
+ my @result;
+ $loop->create_thread(
+ code => sub { return "A result" },
+ on_joined => sub { @result = @_ },
+ );
+
+ wait_for { @result };
+
+ is_deeply( \@result, [ return => "A result" ], 'result to on_joined for returning thread' );
+}
+
+# thread in list context
+{
+ my @result;
+ $loop->create_thread(
+ code => sub { return "A result", "of many", "values" },
+ context => "list",
+ on_joined => sub { @result = @_ },
+ );
+
+ wait_for { @result };
+
+ is_deeply( \@result, [ return => "A result", "of many", "values" ], 'result to on_joined for returning thread in list context' );
+}
+
+# thread that dies
+{
+ my @result;
+ $loop->create_thread(
+ code => sub { die "Ooops I fail\n" },
+ on_joined => sub { @result = @_ },
+ );
+
+ wait_for { @result };
+
+ is_deeply( \@result, [ died => "Ooops I fail\n" ], 'result to on_joined for a died thread' );
+}
+
+done_testing;
diff --git a/t/40channel.t b/t/40channel.t
new file mode 100644
index 0000000..930129b
--- /dev/null
+++ b/t/40channel.t
@@ -0,0 +1,263 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+use Test::Identity;
+
+use IO::Async::Channel;
+
+use IO::Async::OS;
+
+use IO::Async::Loop;
+use Storable qw( freeze );
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+# sync->sync - mostly doesn't involve IO::Async
+{
+ my ( $pipe_rd, $pipe_wr ) = IO::Async::OS->pipepair;
+
+ my $channel_rd = IO::Async::Channel->new;
+ $channel_rd->setup_sync_mode( $pipe_rd );
+
+ my $channel_wr = IO::Async::Channel->new;
+ $channel_wr->setup_sync_mode( $pipe_wr );
+
+ $channel_wr->send( [ structure => "here" ] );
+
+ is_deeply( $channel_rd->recv, [ structure => "here" ], 'Sync mode channels can send/recv structures' );
+
+ $channel_wr->send_frozen( freeze [ prefrozen => "data" ] );
+
+ is_deeply( $channel_rd->recv, [ prefrozen => "data" ], 'Sync mode channels can send_frozen' );
+
+ $channel_wr->close;
+
+ is( $channel_rd->recv, undef, 'Sync mode can be closed' );
+}
+
+# async->sync
+{
+ my ( $pipe_rd, $pipe_wr ) = IO::Async::OS->pipepair;
+
+ my $channel_rd = IO::Async::Channel->new;
+ $channel_rd->setup_sync_mode( $pipe_rd );
+
+ my $channel_wr = IO::Async::Channel->new;
+ $channel_wr->setup_async_mode( write_handle => $pipe_wr );
+
+ $loop->add( $channel_wr );
+
+ $channel_wr->send( [ data => "by async" ] );
+
+ # Cheat for semi-sync
+ my $flushed;
+ $channel_wr->{stream}->write( "", on_flush => sub { $flushed++ } );
+ wait_for { $flushed };
+
+ is_deeply( $channel_rd->recv, [ data => "by async" ], 'Async mode channel can send' );
+
+ $channel_wr->close;
+
+ is( $channel_rd->recv, undef, 'Sync mode can be closed' );
+}
+
+# sync->async configured on_recv
+{
+ my ( $pipe_rd, $pipe_wr ) = IO::Async::OS->pipepair;
+
+ my @recv_queue;
+ my $recv_eof;
+
+ my $channel_rd = IO::Async::Channel->new;
+ $channel_rd->setup_async_mode( read_handle => $pipe_rd );
+
+ $loop->add( $channel_rd );
+
+ $channel_rd->configure(
+ on_recv => sub {
+ identical( $_[0], $channel_rd, 'Channel passed to on_recv' );
+ push @recv_queue, $_[1];
+ },
+ on_eof => sub {
+ $recv_eof++;
+ },
+ );
+
+ my $channel_wr = IO::Async::Channel->new;
+ $channel_wr->setup_sync_mode( $pipe_wr );
+
+ $channel_wr->send( [ data => "by sync" ] );
+
+ wait_for { @recv_queue };
+
+ is_deeply( shift @recv_queue, [ data => "by sync" ], 'Async mode channel can on_recv' );
+
+ $channel_wr->close;
+
+ wait_for { $recv_eof };
+ is( $recv_eof, 1, 'Async mode channel can on_eof' );
+}
+
+# sync->async oneshot ->recv with future
+{
+ my ( $pipe_rd, $pipe_wr ) = IO::Async::OS->pipepair;
+
+ my $channel_rd = IO::Async::Channel->new;
+ $channel_rd->setup_async_mode( read_handle => $pipe_rd );
+
+ $loop->add( $channel_rd );
+
+ my $channel_wr = IO::Async::Channel->new;
+ $channel_wr->setup_sync_mode( $pipe_wr );
+
+ $channel_wr->send( [ data => "by sync" ] );
+
+ my $recv_f = $channel_rd->recv;
+
+ wait_for { $recv_f->is_ready };
+
+ is_deeply( scalar $recv_f->get, [ data => "by sync" ], 'Async mode future can receive data' );
+
+ $channel_wr->close;
+
+ my $eof_f = $channel_rd->recv;
+
+ wait_for { $eof_f->is_ready };
+
+ is( ( $eof_f->failure )[1], "eof", 'Async mode future can receive EOF' );
+}
+
+# sync->async oneshot ->recv with callbacks
+{
+ my ( $pipe_rd, $pipe_wr ) = IO::Async::OS->pipepair;
+
+ my $channel_rd = IO::Async::Channel->new;
+ $channel_rd->setup_async_mode( read_handle => $pipe_rd );
+
+ $loop->add( $channel_rd );
+
+ my $channel_wr = IO::Async::Channel->new;
+ $channel_wr->setup_sync_mode( $pipe_wr );
+
+ $channel_wr->send( [ data => "by sync" ] );
+
+ my $recved;
+ $channel_rd->recv(
+ on_recv => sub {
+ identical( $_[0], $channel_rd, 'Channel passed to ->recv on_recv' );
+ $recved = $_[1];
+ },
+ on_eof => sub { die "Test failed early" },
+ );
+
+ wait_for { $recved };
+
+ is_deeply( $recved, [ data => "by sync" ], 'Async mode channel can ->recv on_recv' );
+
+ $channel_wr->close;
+
+ my $recv_eof;
+ $channel_rd->recv(
+ on_recv => sub { die "Channel recv'ed when not expecting" },
+ on_eof => sub { $recv_eof++ },
+ );
+
+ wait_for { $recv_eof };
+ is( $recv_eof, 1, 'Async mode channel can ->recv on_eof' );
+}
+
+# sync->async write once then close
+{
+ my ( $pipe_rd, $pipe_wr ) = IO::Async::OS->pipepair;
+
+ my $channel_rd = IO::Async::Channel->new;
+ $channel_rd->setup_async_mode( read_handle => $pipe_rd );
+
+ $loop->add( $channel_rd );
+
+ my $channel_wr = IO::Async::Channel->new;
+ $channel_wr->setup_sync_mode( $pipe_wr );
+
+ $channel_wr->send( [ "One value here" ] );
+ $channel_wr->close;
+ undef $channel_wr;
+
+ my $recved;
+ $channel_rd->recv(
+ on_recv => sub {
+ $recved = $_[1];
+ },
+ on_eof => sub { die "Test failed early" },
+ );
+
+ wait_for { $recved };
+
+ is( $recved->[0], "One value here", 'Async mode channel can ->recv buffer at EOF' );
+
+ $loop->remove( $channel_rd );
+}
+
+# Async ->recv cancellation
+{
+ my ( $pipe_rd, $pipe_wr ) = IO::Async::OS->pipepair;
+
+ my $channel_rd = IO::Async::Channel->new;
+ $channel_rd->setup_async_mode( read_handle => $pipe_rd );
+
+ $loop->add( $channel_rd );
+
+ my $channel_wr = IO::Async::Channel->new;
+ $channel_wr->setup_sync_mode( $pipe_wr );
+
+ $channel_wr->send( [ "first" ] );
+ $channel_wr->send( [ "second" ] );
+
+ my $r1_f = $channel_rd->recv;
+ my $r2_f = $channel_rd->recv;
+
+ $r1_f->cancel;
+
+ wait_for { $r2_f->is_ready };
+
+ is_deeply( scalar $r2_f->get, [ "second" ], 'Async recv result after cancellation' );
+
+ $loop->remove( $channel_rd );
+}
+
+# Sereal encoder
+SKIP: {
+ skip "Sereal is not available", 1 unless eval { require Sereal::Encoder; require Sereal::Decoder; };
+
+ my ( $pipe_rd, $pipe_wr ) = IO::Async::OS->pipepair;
+
+ my $channel_rd = IO::Async::Channel->new(
+ codec => "Sereal"
+ );
+ $channel_rd->setup_async_mode( read_handle => $pipe_rd );
+
+ $loop->add( $channel_rd );
+
+ my $channel_wr = IO::Async::Channel->new(
+ codec => "Sereal",
+ );
+ $channel_wr->setup_sync_mode( $pipe_wr );
+
+ $channel_wr->send( [ data => "by sync" ] );
+
+ my $recv_f = $channel_rd->recv;
+
+ wait_for { $recv_f->is_ready };
+
+ is_deeply( scalar $recv_f->get, [ data => "by sync" ], 'Channel can use Sereal as codec' );
+
+ $loop->remove( $channel_rd );
+}
+
+done_testing;
diff --git a/t/41routine.t b/t/41routine.t
new file mode 100644
index 0000000..945a1ec
--- /dev/null
+++ b/t/41routine.t
@@ -0,0 +1,322 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+use Test::Identity;
+use Test::Refcount;
+
+use IO::Async::Routine;
+
+use IO::Async::Channel;
+use IO::Async::Loop;
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+sub test_with_model
+{
+ my ( $model ) = @_;
+
+ {
+ my $calls = IO::Async::Channel->new;
+ my $returns = IO::Async::Channel->new;
+
+ my $routine = IO::Async::Routine->new(
+ model => $model,
+ channels_in => [ $calls ],
+ channels_out => [ $returns ],
+ code => sub {
+ while( my $args = $calls->recv ) {
+ last if ref $args eq "SCALAR";
+
+ my $ret = 0;
+ $ret += $_ for @$args;
+ $returns->send( \$ret );
+ }
+ },
+ on_finish => sub {},
+ );
+
+ isa_ok( $routine, "IO::Async::Routine", "\$routine for $model model" );
+ is_oneref( $routine, "\$routine has refcount 1 initially for $model model" );
+
+ $loop->add( $routine );
+
+ is_refcount( $routine, 2, "\$routine has refcount 2 after \$loop->add for $model model" );
+
+ is( $routine->model, $model, "\$routine->model for $model model" );
+
+ $calls->send( [ 1, 2, 3 ] );
+
+ my $f = $returns->recv;
+
+ wait_for { $f->is_ready };
+
+ my $result = $f->get;
+ is( ${$result}, 6, "Result for $model model" );
+
+ is_refcount( $routine, 2, '$routine has refcount 2 before $loop->remove' );
+
+ $loop->remove( $routine );
+
+ is_oneref( $routine, '$routine has refcount 1 before EOF' );
+ }
+
+ {
+ my $returned;
+ my $return_routine = IO::Async::Routine->new(
+ model => $model,
+ code => sub { return 23 },
+ on_return => sub { $returned = $_[1]; },
+ );
+
+ $loop->add( $return_routine );
+
+ wait_for { defined $returned };
+
+ is( $returned, 23, "on_return for $model model" );
+
+ my $died;
+ my $die_routine = IO::Async::Routine->new(
+ model => $model,
+ code => sub { die "ARGH!\n" },
+ on_die => sub { $died = $_[1]; },
+ );
+
+ $loop->add( $die_routine );
+
+ wait_for { defined $died };
+
+ is( $died, "ARGH!\n", "on_die for $model model" );
+ }
+
+ {
+ my $channel = IO::Async::Channel->new;
+
+ my $finished;
+ my $routine = IO::Async::Routine->new(
+ model => $model,
+ channels_in => [ $channel ],
+ code => sub { while( $channel->recv ) { 1 } },
+ on_finish => sub { $finished++ },
+ );
+
+ $loop->add( $routine );
+
+ $channel->close;
+
+ wait_for { $finished };
+ pass( "Recv on closed channel for $model model" );
+ }
+
+ {
+ my $channel = IO::Async::Channel->new;
+
+ my $routine = IO::Async::Routine->new(
+ model => $model,
+ channels_out => [ $channel ],
+ code => sub {
+ $SIG{INT} = sub { $channel->send( \"SIGINT" ); die "SIGINT" };
+ $channel->send( \"READY" );
+
+ # Busy-wait so thread kill still works
+ my $until = time() + 5;
+ 1 while time() < $until;
+ },
+ );
+
+ $loop->add( $routine );
+
+ my $f;
+ $f = $channel->recv;
+
+ wait_for { $f->is_ready };
+
+ is( ${ $f->get }, "READY", 'Routine is ready for SIGINT' );
+
+ $routine->kill( "INT" );
+
+ $f = $channel->recv;
+
+ wait_for { $f->is_ready };
+
+ is( ${ $f->get }, "SIGINT", 'Routine caught SIGINT' );
+ }
+}
+
+foreach my $model (qw( fork thread )) {
+ SKIP: {
+ skip "This Perl does not support threads", 9
+ if $model eq "thread" and not IO::Async::OS->HAVE_THREADS;
+ skip "This Perl does not support fork()", 9
+ if $model eq "fork" and not IO::Async::OS->HAVE_POSIX_FORK;
+
+ test_with_model( $model );
+ }
+}
+
+# multiple channels in and out
+{
+ my $in1 = IO::Async::Channel->new;
+ my $in2 = IO::Async::Channel->new;
+ my $out1 = IO::Async::Channel->new;
+ my $out2 = IO::Async::Channel->new;
+
+ my $routine = IO::Async::Routine->new(
+ channels_in => [ $in1, $in2 ],
+ channels_out => [ $out1, $out2 ],
+ code => sub {
+ while( my $op = $in1->recv ) {
+ $op = $$op; # deref
+ $out1->send( \"Ready $op" );
+ my @args = @{ $in2->recv };
+ my $result = $op eq "+" ? $args[0] + $args[1]
+ : "ERROR";
+ $out2->send( \$result );
+ }
+ },
+ on_finish => sub { },
+ );
+
+ isa_ok( $routine, "IO::Async::Routine", '$routine' );
+
+ $loop->add( $routine );
+
+ $in1->send( \"+" );
+
+ my $status_f = $out1->recv;
+
+ wait_for { $status_f->is_ready };
+ is( ${ $status_f->get }, "Ready +", '$status_f result midway through Routine' );
+
+ $in2->send( [ 10, 20 ] );
+
+ my $result_f = $out2->recv;
+
+ wait_for { $result_f->is_ready };
+
+ is( ${ $result_f->get }, 30, '$result_f result at end of Routine' );
+
+ $loop->remove( $routine );
+}
+
+# sharing a Channel between Routines
+{
+ my $channel = IO::Async::Channel->new;
+
+ my $src_finished;
+ my $src_routine = IO::Async::Routine->new(
+ channels_out => [ $channel ],
+ code => sub {
+ $channel->send( [ some => "data" ] );
+ return 0;
+ },
+ on_finish => sub { $src_finished++ },
+ on_die => sub { die "source routine failed - $_[1]" },
+ );
+
+ $loop->add( $src_routine );
+
+ my $sink_result;
+ my $sink_routine = IO::Async::Routine->new(
+ channels_in => [ $channel ],
+ code => sub {
+ my @data = @{ $channel->recv };
+ return ( $data[0] eq "some" and $data[1] eq "data" ) ? 0 : 1;
+ },
+ on_return => sub { $sink_result = $_[1] },
+ on_die => sub { die "sink routine failed - $_[1]" },
+ );
+
+ $loop->add( $sink_routine );
+
+ wait_for { $src_finished and defined $sink_result };
+
+ is( $sink_result, 0, 'synchronous src->sink can share a channel' );
+}
+
+# Test that 'setup' works
+SKIP: {
+ skip "This Perl does not support fork()", 1
+ if not IO::Async::OS->HAVE_POSIX_FORK;
+
+ my $channel = IO::Async::Channel->new;
+
+ my $routine = IO::Async::Routine->new(
+ model => "fork",
+ setup => [
+ env => { FOO => "Here is a random string" },
+ ],
+
+ channels_out => [ $channel ],
+ code => sub {
+ $channel->send( [ $ENV{FOO} ] );
+ $channel->close;
+ return 0;
+ },
+ on_finish => sub {},
+ );
+
+ $loop->add( $routine );
+
+ my $f = $channel->recv;
+
+ wait_for { $f->is_ready };
+
+ my $result = $f->get;
+ is( $result->[0], "Here is a random string", '$result from Routine with modified ENV' );
+
+ $loop->remove( $routine );
+}
+
+# Test that STDOUT/STDERR are unaffected
+SKIP: {
+ skip "This Perl does not support fork()", 1
+ if not IO::Async::OS->HAVE_POSIX_FORK;
+
+ my ( $pipe_rd, $pipe_wr ) = IO::Async::OS->pipepair;
+
+ my $routine;
+ {
+ open my $stdoutsave, ">&", \*STDOUT;
+ POSIX::dup2( $pipe_wr->fileno, STDOUT->fileno );
+
+ open my $stderrsave, ">&", \*STDERR;
+ POSIX::dup2( $pipe_wr->fileno, STDERR->fileno );
+
+ $routine = IO::Async::Routine->new(
+ model => "fork",
+ code => sub {
+ STDOUT->autoflush(1);
+ print STDOUT "A line to STDOUT\n";
+ print STDERR "A line to STDERR\n";
+ return 0;
+ }
+ );
+
+ $loop->add( $routine );
+
+ POSIX::dup2( $stdoutsave->fileno, STDOUT->fileno );
+ POSIX::dup2( $stderrsave->fileno, STDERR->fileno );
+ }
+
+ my $buffer = "";
+ $loop->watch_io(
+ handle => $pipe_rd,
+ on_read_ready => sub { sysread $pipe_rd, $buffer, 8192, length $buffer or die "Cannot read - $!" },
+ );
+
+ wait_for { $buffer =~ m/\n.*\n/ };
+
+ is( $buffer, "A line to STDOUT\nA line to STDERR\n", 'Write-to-STD{OUT+ERR} wrote to pipe' );
+
+ $loop->unwatch_io( handle => $pipe_rd, on_read_ready => 1 );
+ $loop->remove( $routine );
+}
+
+done_testing;
diff --git a/t/42function.t b/t/42function.t
new file mode 100644
index 0000000..f4b1c4d
--- /dev/null
+++ b/t/42function.t
@@ -0,0 +1,569 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+use Test::Fatal;
+use Test::Refcount;
+use constant HAVE_TEST_MEMORYGROWTH => eval { require Test::MemoryGrowth; };
+
+use File::Temp qw( tempdir );
+use Time::HiRes qw( sleep );
+
+use IO::Async::Function;
+
+use IO::Async::OS;
+
+use IO::Async::Loop;
+
+use constant AUT => $ENV{TEST_QUICK_TIMERS} ? 0.1 : 1;
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+# by future
+{
+ my $function = IO::Async::Function->new(
+ min_workers => 1,
+ max_workers => 1,
+ code => sub { return $_[0] + $_[1] },
+ );
+
+ ok( defined $function, '$function defined' );
+ isa_ok( $function, "IO::Async::Function", '$function isa IO::Async::Function' );
+
+ is_oneref( $function, '$function has refcount 1' );
+
+ $loop->add( $function );
+
+ is_refcount( $function, 2, '$function has refcount 2 after $loop->add' );
+
+ is( $function->workers, 1, '$function has 1 worker' );
+ is( $function->workers_busy, 0, '$function has 0 workers busy' );
+ is( $function->workers_idle, 1, '$function has 1 workers idle' );
+
+ my $future = $function->call(
+ args => [ 10, 20 ],
+ );
+
+ isa_ok( $future, "Future", '$future' );
+
+ is_refcount( $function, 2, '$function has refcount 2 after ->call' );
+
+ is( $function->workers_busy, 1, '$function has 1 worker busy after ->call' );
+ is( $function->workers_idle, 0, '$function has 0 worker idle after ->call' );
+
+ wait_for { $future->is_ready };
+
+ my ( $result ) = $future->get;
+
+ is( $result, 30, '$result after call returns by future' );
+
+ is( $function->workers_busy, 0, '$function has 0 workers busy after call returns' );
+ is( $function->workers_idle, 1, '$function has 1 workers idle after call returns' );
+
+ $loop->remove( $function );
+}
+
+# by callback
+{
+ my $function = IO::Async::Function->new(
+ min_workers => 1,
+ max_workers => 1,
+ code => sub { return $_[0] + $_[1] },
+ );
+
+ $loop->add( $function );
+
+ my $result;
+
+ $function->call(
+ args => [ 10, 20 ],
+ on_return => sub { $result = shift },
+ on_error => sub { die "Test failed early - @_" },
+ );
+
+ wait_for { defined $result };
+
+ is( $result, 30, '$result after call returns by callback' );
+
+ $loop->remove( $function );
+}
+
+# Test queueing
+{
+ my $function = IO::Async::Function->new(
+ min_workers => 1,
+ max_workers => 1,
+ code => sub { return $_[0] + $_[1] },
+ );
+
+ $loop->add( $function );
+
+ my @result;
+
+ my $f1 = $function->call(
+ args => [ 1, 2 ],
+ on_return => sub { push @result, shift },
+ on_error => sub { die "Test failed early - @_" },
+ );
+ my $f2 = $function->call(
+ args => [ 3, 4 ],
+ on_return => sub { push @result, shift },
+ on_error => sub { die "Test failed early - @_" },
+ );
+
+ is( $function->workers, 1, '$function->workers is still 1 after 2 calls' );
+
+ isa_ok( $f1, "Future", '$f1' );
+ isa_ok( $f2, "Future", '$f2' );
+
+ wait_for { @result == 2 };
+
+ is_deeply( \@result, [ 3, 7 ], '@result after both calls return' );
+
+ is( $function->workers, 1, '$function->workers is still 1 after 2 calls return' );
+
+ $loop->remove( $function );
+}
+
+# References
+{
+ my $function = IO::Async::Function->new(
+ code => sub { return ref( $_[0] ), \$_[1] },
+ );
+
+ $loop->add( $function );
+
+ my @result;
+
+ $function->call(
+ args => [ \'a', 'b' ],
+ on_return => sub { @result = @_ },
+ on_error => sub { die "Test failed early - @_" },
+ );
+
+ wait_for { scalar @result };
+
+ is_deeply( \@result, [ 'SCALAR', \'b' ], 'Call and result preserves references' );
+
+ $loop->remove( $function );
+}
+
+# Exception throwing
+{
+ my $line = __LINE__ + 2;
+ my $function = IO::Async::Function->new(
+ code => sub { die shift },
+ );
+
+ $loop->add( $function );
+
+ my $err;
+
+ my $f = $function->call(
+ args => [ "exception name" ],
+ on_return => sub { },
+ on_error => sub { $err = shift },
+ );
+
+ wait_for { defined $err };
+
+ like( $err, qr/^exception name at \Q$0\E line \d+\.$/, '$err after exception' );
+
+ is_deeply( [ $f->failure ],
+ [ "exception name at $0 line $line.", error => ],
+ '$f->failure after exception' );
+
+ $loop->remove( $function );
+}
+
+# max_workers
+{
+ my $count = 0;
+
+ my $function = IO::Async::Function->new(
+ max_workers => 1,
+ code => sub { $count++; die "$count\n" },
+ exit_on_die => 0,
+ );
+
+ $loop->add( $function );
+
+ my @errs;
+ $function->call(
+ args => [],
+ on_return => sub { },
+ on_error => sub { push @errs, shift },
+ );
+ $function->call(
+ args => [],
+ on_return => sub { },
+ on_error => sub { push @errs, shift },
+ );
+
+ undef @errs;
+ wait_for { scalar @errs == 2 };
+
+ is_deeply( \@errs, [ "1", "2" ], 'Closed variables preserved when exit_on_die => 0' );
+
+ $loop->remove( $function );
+}
+
+# exit_on_die
+{
+ my $count = 0;
+
+ my $function = IO::Async::Function->new(
+ max_workers => 1,
+ code => sub { $count++; die "$count\n" },
+ exit_on_die => 1,
+ );
+
+ $loop->add( $function );
+
+ my @errs;
+ $function->call(
+ args => [],
+ on_return => sub { },
+ on_error => sub { push @errs, shift },
+ );
+ $function->call(
+ args => [],
+ on_return => sub { },
+ on_error => sub { push @errs, shift },
+ );
+
+ undef @errs;
+ wait_for { scalar @errs == 2 };
+
+ is_deeply( \@errs, [ "1", "1" ], 'Closed variables preserved when exit_on_die => 1' );
+
+ $loop->remove( $function );
+}
+
+# restart after exit
+SKIP: {
+ skip "This Perl does not support fork()", 4
+ if not IO::Async::OS->HAVE_POSIX_FORK;
+
+ my $function = IO::Async::Function->new(
+ model => "fork",
+ min_workers => 0,
+ max_workers => 1,
+ code => sub { $_[0] ? exit shift : return 0 },
+ );
+
+ $loop->add( $function );
+
+ my $err;
+
+ $function->call(
+ args => [ 16 ],
+ on_return => sub { $err = "" },
+ on_error => sub { $err = [ @_ ] },
+ );
+
+ wait_for { defined $err };
+
+ # Not sure what reason we might get - need to check both
+ ok( $err->[0] eq "closed" || $err->[0] eq "exit", '$err->[0] after child death' )
+ or diag( 'Expected "closed" or "exit", found ' . $err->[0] );
+
+ is( scalar $function->workers, 0, '$function->workers is now 0' );
+
+ $function->call(
+ args => [ 0 ],
+ on_return => sub { $err = "return" },
+ on_error => sub { $err = [ @_ ] },
+ );
+
+ is( scalar $function->workers, 1, '$function->workers is now 1 again' );
+
+ undef $err;
+ wait_for { defined $err };
+
+ is( $err, "return", '$err is "return" after child nondeath' );
+
+ $loop->remove( $function );
+}
+
+## Now test that parallel runs really are parallel
+{
+ # touch $dir/$n in each worker, touch $dir/done to finish it
+ sub touch
+ {
+ my ( $file ) = @_;
+
+ open( my $fh, ">", $file ) or die "Cannot write $file - $!";
+ close( $fh );
+ }
+
+ my $function = IO::Async::Function->new(
+ min_workers => 3,
+ code => sub {
+ my ( $dir, $n ) = @_;
+ my $file = "$dir/$n";
+
+ touch( $file );
+
+ # Wait for synchronisation
+ sleep 0.1 while ! -e "$dir/done";
+
+ unlink( $file );
+
+ return $n;
+ },
+ );
+
+ $loop->add( $function );
+
+ is( scalar $function->workers, 3, '$function->workers is 3' );
+
+ my $dir = tempdir( CLEANUP => 1 );
+
+ my %ret;
+
+ foreach my $id ( 1, 2, 3 ) {
+ $function->call(
+ args => [ $dir, $id ],
+ on_return => sub { $ret{$id} = shift },
+ on_error => sub { die "Test failed early - @_" },
+ );
+ }
+
+ wait_for { -e "$dir/1" and -e "$dir/2" and -e "$dir/3" };
+
+ ok( 1, 'synchronise files created' );
+
+ # Synchronize deleting them;
+ touch( "$dir/done" );
+
+ undef %ret;
+ wait_for { keys %ret == 3 };
+
+ unlink( "$dir/done" );
+
+ is_deeply( \%ret, { 1 => 1, 2 => 2, 3 => 3 }, 'ret keys after parallel run' );
+
+ is( scalar $function->workers, 3, '$function->workers is still 3' );
+
+ $loop->remove( $function );
+}
+
+# Test for idle timeout
+{
+ my $function = IO::Async::Function->new(
+ min_workers => 0,
+ max_workers => 1,
+ idle_timeout => 2 * AUT,
+ code => sub { return $_[0] },
+ );
+
+ $loop->add( $function );
+
+ my $result;
+
+ $function->call(
+ args => [ 1 ],
+ on_result => sub { $result = $_[0] },
+ );
+
+ wait_for { defined $result };
+
+ is( $function->workers, 1, '$function has 1 worker after call' );
+
+ my $waited;
+ $loop->watch_time( after => 1 * AUT, code => sub { $waited++ } );
+
+ wait_for { $waited };
+
+ is( $function->workers, 1, '$function still has 1 worker after short delay' );
+
+ undef $result;
+ $function->call(
+ args => [ 1 ],
+ on_result => sub { $result = $_[0] },
+ );
+
+ wait_for { defined $result };
+
+ undef $waited;
+ $loop->watch_time( after => 3 * AUT, code => sub { $waited++ } );
+
+ wait_for { $waited };
+
+ is( $function->workers, 0, '$function has 0 workers after longer delay' );
+
+ $loop->remove( $function );
+}
+
+# Restart
+{
+ my $value = 1;
+
+ my $function = IO::Async::Function->new(
+ code => sub { return $value },
+ );
+
+ $loop->add( $function );
+
+ my $result;
+ $function->call(
+ args => [],
+ on_return => sub { $result = shift },
+ on_error => sub { die "Test failed early - @_" },
+ );
+
+ wait_for { defined $result };
+
+ is( $result, 1, '$result before restart' );
+
+ $value = 2;
+ $function->restart;
+
+ undef $result;
+ $function->call(
+ args => [],
+ on_return => sub { $result = shift },
+ on_error => sub { die "Test failed early - @_" },
+ );
+
+ wait_for { defined $result };
+
+ is( $result, 2, '$result after restart' );
+
+ undef $result;
+ $function->call(
+ args => [],
+ on_return => sub { $result = shift },
+ on_error => sub { die "Test failed early - @_" },
+ );
+
+ $function->restart;
+
+ wait_for { defined $result };
+
+ is( $result, 2, 'call before restart still returns result' );
+
+ $loop->remove( $function );
+}
+
+# max_worker_calls
+{
+ my $counter;
+ my $function = IO::Async::Function->new(
+ max_workers => 1,
+ max_worker_calls => 2,
+ code => sub { return ++$counter; }
+ );
+
+ $loop->add( $function );
+
+ my $result;
+ $function->call(
+ args => [],
+ on_return => sub { $result = shift },
+ on_error => sub { die "Test failed early - @_" },
+ );
+ wait_for { defined $result };
+ is( $result, 1, '$result from first call' );
+
+ undef $result;
+ $function->call(
+ args => [],
+ on_return => sub { $result = shift },
+ on_error => sub { die "Test failed early - @_" },
+ );
+ wait_for { defined $result };
+ is( $result, 2, '$result from second call' );
+
+ undef $result;
+ $function->call(
+ args => [],
+ on_return => sub { $result = shift },
+ on_error => sub { die "Test failed early - @_" },
+ );
+ wait_for { defined $result };
+ is( $result, 1, '$result from third call' );
+
+ $loop->remove( $function );
+}
+
+# Cancellation of sent calls
+{
+ my $function = IO::Async::Function->new(
+ max_workers => 1,
+ code => sub {
+ return 123;
+ },
+ );
+
+ $loop->add( $function );
+
+ my $f1 = $function->call( args => [] );
+ $f1->cancel;
+
+ my $f2 = $function->call( args => [] );
+
+ wait_for { $f2->is_ready };
+
+ is( scalar $f2->get, 123, 'Result of function call after cancelled call' );
+
+ $loop->remove( $function );
+}
+
+# Cancellation of pending calls
+{
+ my $function = IO::Async::Function->new(
+ max_workers => 1,
+ code => do { my $state; sub {
+ my $oldstate = $state;
+ $state = shift;
+ return $oldstate;
+ } },
+ );
+
+ $loop->add( $function );
+
+ # Queue 3 calls but immediately cancel the middle one
+ my ( $f1, $f2, $f3 ) = map {
+ $function->call( args => [ $_ ] )
+ } 1 .. 3;
+
+ $f2->cancel;
+
+ wait_for { $f1->is_ready and $f3->is_ready };
+
+ is( scalar $f1->get, undef, '$f1 result is undef' );
+ is( scalar $f3->get, 1, '$f3 result is 1' );
+
+ $loop->remove( $function );
+}
+
+# Leak test (RT99552)
+if( HAVE_TEST_MEMORYGROWTH ) {
+ diag( "Performing memory leak test" );
+
+ my $function = IO::Async::Function->new(
+ max_workers => 8,
+ code => sub {},
+ );
+
+ $loop->add( $function );
+
+ Test::MemoryGrowth::no_growth( sub {
+ $function->restart;
+ $function->call( args => [] )->get;
+ }, calls => 100,
+ 'IO::Async::Function calls do not leak memory' );
+
+ $loop->remove( $function );
+ undef $function;
+}
+
+done_testing;
diff --git a/t/50resolver.t b/t/50resolver.t
new file mode 100644
index 0000000..c73bc0f
--- /dev/null
+++ b/t/50resolver.t
@@ -0,0 +1,389 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+
+use Socket 1.93 qw(
+ AF_INET SOCK_STREAM INADDR_LOOPBACK AI_PASSIVE
+ pack_sockaddr_in getaddrinfo getnameinfo
+);
+
+use IO::Async::Loop;
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+my $resolver = $loop->resolver;
+isa_ok( $resolver, "IO::Async::Resolver", '$loop->resolver' );
+
+SKIP: {
+ my @pwuid;
+ defined eval { @pwuid = getpwuid( $< ) } or
+ skip "No getpwuid()", 5;
+
+ {
+ my $future = $resolver->resolve(
+ type => 'getpwuid',
+ data => [ $< ],
+ );
+
+ isa_ok( $future, "Future", '$future' );
+
+ wait_for { $future->is_ready };
+
+ my @result = $future->get;
+
+ is_deeply( \@result, \@pwuid, 'getpwuid from future' );
+ }
+
+ {
+ my $result;
+
+ $resolver->resolve(
+ type => 'getpwuid',
+ data => [ $< ],
+ on_resolved => sub { $result = [ @_ ] },
+ on_error => sub { die "Test died early" },
+ );
+
+ wait_for { $result };
+
+ is_deeply( $result, \@pwuid, 'getpwuid' );
+ }
+
+ {
+ my $result;
+
+ $loop->resolve(
+ type => 'getpwuid',
+ data => [ $< ],
+ on_resolved => sub { $result = [ @_ ] },
+ on_error => sub { die "Test died early" },
+ );
+
+ wait_for { $result };
+
+ is_deeply( $result, \@pwuid, 'getpwuid via $loop->resolve' );
+ }
+
+ SKIP: {
+ my $user_name = $pwuid[0];
+ skip "getpwnam - No user name", 1 unless defined $user_name;
+
+ my @pwnam = getpwnam( $user_name );
+
+ my $result;
+
+ $resolver->resolve(
+ type => 'getpwnam',
+ data => [ $user_name ],
+ on_resolved => sub { $result = [ @_ ] },
+ on_error => sub { die "Test died early" },
+ );
+
+ wait_for { $result };
+
+ is_deeply( $result, \@pwnam, 'getpwnam' );
+ }
+}
+
+my @proto = getprotobyname( "tcp" );
+
+{
+ my $result;
+
+ $resolver->resolve(
+ type => 'getprotobyname',
+ data => [ "tcp" ],
+ on_resolved => sub { $result = [ @_ ] },
+ on_error => sub { die "Test died early" },
+ );
+
+ wait_for { $result };
+
+ is_deeply( $result, \@proto, 'getprotobyname' );
+}
+
+SKIP: {
+ my $proto_number = $proto[2];
+ skip "getprotobynumber - No protocol number", 1 unless defined $proto_number;
+
+ my @proto = getprotobynumber( $proto_number );
+
+ my $result;
+
+ $resolver->resolve(
+ type => 'getprotobynumber',
+ data => [ $proto_number ],
+ on_resolved => sub { $result = [ @_ ] },
+ on_error => sub { die "Test died early" },
+ );
+
+ wait_for { $result };
+
+ is_deeply( $result, \@proto, 'getprotobynumber' );
+}
+
+# Some systems seem to mangle the order of results between PF_INET and
+# PF_INET6 depending on who asks. We'll hint AF_INET + SOCK_STREAM to minimise
+# the risk of a spurious test failure because of ordering issues
+
+my ( $localhost_err, @localhost_addrs ) = getaddrinfo( "localhost", "www", { family => AF_INET, socktype => SOCK_STREAM } );
+
+{
+ my $result;
+
+ $resolver->resolve(
+ type => 'getaddrinfo_array',
+ data => [ "localhost", "www", "inet", "stream" ],
+ on_resolved => sub { $result = [ 'resolved', @_ ] },
+ on_error => sub { $result = [ 'error', @_ ] },
+ );
+
+ wait_for { $result };
+
+ if( $localhost_err ) {
+ is( $result->[0], "error", 'getaddrinfo_array - error' );
+ is_deeply( $result->[1], "$localhost_err\n", 'getaddrinfo_array - error message' );
+ }
+ else {
+ is( $result->[0], "resolved", 'getaddrinfo_array - resolved' );
+
+ my @got = @{$result}[1..$#$result];
+ my @expect = map { [ @{$_}{qw( family socktype protocol addr canonname )} ] } @localhost_addrs;
+
+ is_deeply( \@got, \@expect, 'getaddrinfo_array - resolved addresses' );
+ }
+}
+
+{
+ my $result;
+
+ $resolver->resolve(
+ type => 'getaddrinfo_hash',
+ data => [ host => "localhost", service => "www", family => "inet", socktype => "stream" ],
+ on_resolved => sub { $result = [ 'resolved', @_ ] },
+ on_error => sub { $result = [ 'error', @_ ] },
+ );
+
+ wait_for { $result };
+
+ if( $localhost_err ) {
+ is( $result->[0], "error", 'getaddrinfo_hash - error' );
+ is_deeply( $result->[1], "$localhost_err\n", 'getaddrinfo_hash - error message' );
+ }
+ else {
+ is( $result->[0], "resolved", 'getaddrinfo_hash - resolved' );
+
+ my @got = @{$result}[1..$#$result];
+
+ is_deeply( \@got, \@localhost_addrs, 'getaddrinfo_hash - resolved addresses' );
+ }
+}
+
+{
+ my $result;
+
+ $resolver->getaddrinfo(
+ host => "localhost",
+ service => "www",
+ family => "inet",
+ socktype => "stream",
+ on_resolved => sub { $result = [ 'resolved', @_ ] },
+ on_error => sub { $result = [ 'error', @_ ] },
+ );
+
+ wait_for { $result };
+
+ if( $localhost_err ) {
+ is( $result->[0], "error", '$resolver->getaddrinfo - error' );
+ is_deeply( $result->[1], "$localhost_err\n", '$resolver->getaddrinfo - error message' );
+ }
+ else {
+ is( $result->[0], "resolved", '$resolver->getaddrinfo - resolved' );
+
+ my @got = @{$result}[1..$#$result];
+
+ is_deeply( \@got, \@localhost_addrs, '$resolver->getaddrinfo - resolved addresses' );
+ }
+}
+
+{
+ my $future = $resolver->getaddrinfo(
+ host => "localhost",
+ service => "www",
+ family => "inet",
+ socktype => "stream",
+ );
+
+ isa_ok( $future, "Future", '$future for $resolver->getaddrinfo' );
+
+ wait_for { $future->is_ready };
+
+ if( $localhost_err ) {
+ is( scalar $future->failure, "$localhost_err\n", '$resolver->getaddrinfo - error message' );
+ }
+ else {
+ my @got = $future->get;
+
+ is_deeply( \@got, \@localhost_addrs, '$resolver->getaddrinfo - resolved addresses' );
+ }
+}
+
+{
+ my ( $lo_err, @lo_addrs ) = getaddrinfo( "127.0.0.1", "80", { socktype => SOCK_STREAM } );
+
+ my $result;
+
+ $resolver->getaddrinfo(
+ host => "127.0.0.1",
+ service => "80",
+ socktype => SOCK_STREAM,
+ on_resolved => sub { $result = [ 'resolved', @_ ] },
+ on_error => sub { $result = [ 'error', @_ ] },
+ );
+
+ is( $result->[0], 'resolved', '$resolver->getaddrinfo on numeric host/service is synchronous' );
+
+ my @got = @{$result}[1..$#$result];
+
+ is_deeply( \@got, \@lo_addrs, '$resolver->getaddrinfo resolved addresses synchronously' );
+}
+
+{
+ my ( $passive_err, @passive_addrs ) = getaddrinfo( "", "3000", { socktype => SOCK_STREAM, family => AF_INET, flags => AI_PASSIVE } );
+
+ my $result;
+
+ $resolver->getaddrinfo(
+ family => "inet",
+ service => "3000",
+ socktype => "stream",
+ passive => 1,
+ on_resolved => sub { $result = [ 'resolved', @_ ] },
+ on_error => sub { $result = [ 'error', @_ ] },
+ );
+
+ if( $passive_err ) {
+ is( $result->[0], "error", '$resolver->getaddrinfo passive - error synchronously' );
+ is_deeply( $result->[1], "$passive_err\n", '$resolver->getaddrinfo passive - error message' );
+ }
+ else {
+ is( $result->[0], "resolved", '$resolver->getaddrinfo passive - resolved synchronously' );
+
+ my @got = @{$result}[1..$#$result];
+
+ is_deeply( \@got, \@passive_addrs, '$resolver->getaddrinfo passive - resolved addresses' );
+ }
+}
+
+{
+ my ( $lo_err, @lo_addrs ) = getaddrinfo( "127.0.0.1", "80", { socktype => SOCK_STREAM } );
+
+ my $future = $resolver->getaddrinfo(
+ host => "127.0.0.1",
+ service => "80",
+ socktype => SOCK_STREAM,
+ );
+
+ isa_ok( $future, "Future", '$future for $resolver->getaddrinfo numerical' );
+
+ wait_for { $future->is_ready };
+
+ my @got = $future->get;
+
+ is_deeply( \@got, \@lo_addrs, '$resolver->getaddrinfo resolved addresses synchronously' );
+}
+
+my $testaddr = pack_sockaddr_in( 80, INADDR_LOOPBACK );
+my ( $testerr, $testhost, $testserv ) = getnameinfo( $testaddr );
+
+{
+ my $result;
+
+ $resolver->getnameinfo(
+ addr => $testaddr,
+ on_resolved => sub { $result = [ 'resolved', @_ ] },
+ on_error => sub { $result = [ 'error', @_ ] },
+ );
+
+ wait_for { $result };
+
+ if( $testerr ) {
+ is( $result->[0], "error", '$resolver->getnameinfo - error' );
+ is_deeply( $result->[1], "$testerr\n", '$resolver->getnameinfo - error message' );
+ }
+ else {
+ is( $result->[0], "resolved", '$resolver->getnameinfo - resolved' );
+ is_deeply( [ @{$result}[1..2] ], [ $testhost, $testserv ], '$resolver->getnameinfo - resolved names' );
+ }
+}
+
+{
+ my $future = $resolver->getnameinfo(
+ addr => $testaddr,
+ );
+
+ wait_for { $future->is_ready };
+
+ if( $testerr ) {
+ is( scalar $future->failure, "$testerr\n", '$resolver->getnameinfo - error message from future' );
+ }
+ else {
+ my @got = $future->get;
+
+ is_deeply( \@got, [ $testhost, $testserv ], '$resolver->getnameinfo - resolved names from future' );
+ }
+}
+
+{
+ my $result;
+
+ $resolver->getnameinfo(
+ addr => $testaddr,
+ numeric => 1,
+ on_resolved => sub { $result = [ 'resolved', @_ ] },
+ on_error => sub { $result = [ 'error', @_ ] },
+ );
+
+ is_deeply( $result, [ resolved => "127.0.0.1", 80 ], '$resolver->getnameinfo with numeric is synchronous' );
+}
+
+{
+ my $future = $resolver->getnameinfo(
+ addr => $testaddr,
+ numeric => 1,
+ );
+
+ is_deeply( [ $future->get ], [ "127.0.0.1", 80 ], '$resolver->getnameinfo with numeric is synchronous for future' );
+}
+
+# $loop->set_resolver
+{
+ my $callcount = 0;
+ {
+ package MockResolver;
+ use base qw( IO::Async::Notifier );
+
+ sub new { bless {}, shift }
+
+ sub resolve {
+ $callcount++; return Future->done();
+ }
+ sub getaddrinfo {}
+ sub getnameinfo {}
+ }
+
+ $loop->set_resolver( MockResolver->new );
+
+ $loop->resolve( type => "getpwuid", data => [ 0 ] )->get;
+
+ is( $callcount, 1, '$callcount 1 after ->resolve' );
+}
+
+done_testing;
diff --git a/t/51loop-connect.t b/t/51loop-connect.t
new file mode 100644
index 0000000..c217397
--- /dev/null
+++ b/t/51loop-connect.t
@@ -0,0 +1,333 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+use Test::Identity;
+
+use IO::Socket::INET;
+use POSIX qw( ENOENT );
+use Socket qw( AF_UNIX inet_ntoa );
+
+use IO::Async::Loop;
+
+use IO::Async::Stream;
+use IO::Async::Socket;
+
+# Some odd locations like BSD jails might not like INADDR_LOOPBACK. We'll
+# establish a baseline first to test against
+my $INADDR_LOOPBACK = do {
+ my $localsock = IO::Socket::INET->new( LocalAddr => "localhost", Listen => 1 );
+ $localsock->sockaddr;
+};
+my $INADDR_LOOPBACK_HOST = inet_ntoa( $INADDR_LOOPBACK );
+if( $INADDR_LOOPBACK ne INADDR_LOOPBACK ) {
+ diag( "Testing with INADDR_LOOPBACK=$INADDR_LOOPBACK_HOST; this may be because of odd networking" );
+}
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+# Try connect(2)ing to a socket we've just created
+my $listensock = IO::Socket::INET->new(
+ Type => SOCK_STREAM,
+ LocalAddr => 'localhost',
+ LocalPort => 0,
+ Listen => 1
+) or die "Cannot create listensock - $!";
+
+my $addr = $listensock->sockname;
+
+{
+ my $future = $loop->connect(
+ addr => { family => "inet", socktype => "stream", addr => $addr },
+ );
+
+ isa_ok( $future, "Future", '$future' );
+
+ wait_for { $future->is_ready };
+
+ my ( $sock ) = $future->get;
+
+ can_ok( $sock, qw( peerhost peerport ) );
+ is_deeply( [ unpack_sockaddr_in $sock->peername ],
+ [ unpack_sockaddr_in $addr ], 'by addr: $sock->getpeername is $addr from future' );
+
+ $listensock->accept; # Throw it away
+}
+
+# handle
+{
+ my $future = $loop->connect(
+ handle => my $given_stream = IO::Async::Stream->new,
+ addr => { family => "inet", socktype => "stream", addr => $addr },
+ );
+
+ isa_ok( $future, "Future", '$future for ->connect( handle )' );
+
+ wait_for { $future->is_ready };
+
+ my $stream = $future->get;
+ identical( $stream, $given_stream, '$future->get returns given Stream' );
+ ok( my $sock = $stream->read_handle, '$stream has a read handle' );
+ is_deeply( [ unpack_sockaddr_in $sock->peername ],
+ [ unpack_sockaddr_in $addr ], 'Returned $stream->read_handle->getpeername is $addr' );
+
+ $listensock->accept; # Throw it away
+}
+
+# legacy callbacks
+{
+ my $sock;
+
+ $loop->connect(
+ addr => { family => "inet", socktype => "stream", addr => $addr },
+ on_connected => sub { $sock = shift; },
+ on_connect_error => sub { die "Test died early - connect error $_[0]() - $_[-1]\n"; },
+ );
+
+ wait_for { $sock };
+
+ # Not sure if it'll be an IO::Socket::INET or ::IP, but either way it should support these
+ can_ok( $sock, qw( peerhost peerport ) );
+ is_deeply( [ unpack_sockaddr_in $sock->peername ],
+ [ unpack_sockaddr_in $addr ], 'by addr: $sock->getpeername is $addr' );
+
+ $listensock->accept; # Throw it away
+}
+
+# Now try by name
+{
+ my $future = $loop->connect(
+ host => $listensock->sockhost,
+ service => $listensock->sockport,
+ socktype => $listensock->socktype,
+ );
+
+ isa_ok( $future, "Future", '$future' );
+
+ wait_for { $future->is_ready };
+
+ my ( $sock ) = $future->get;
+
+ can_ok( $sock, qw( peerhost peerport ) );
+ is_deeply( [ unpack_sockaddr_in $sock->peername ],
+ [ unpack_sockaddr_in $addr ], 'by host/service: $sock->getpeername is $addr from future' );
+
+ is( $sock->sockhost, $INADDR_LOOPBACK_HOST, '$sock->sockhost is INADDR_LOOPBACK_HOST from future' );
+
+ $listensock->accept; # Throw it away
+}
+
+# legacy callbacks
+{
+ my $sock;
+
+ $loop->connect(
+ host => $listensock->sockhost,
+ service => $listensock->sockport,
+ socktype => $listensock->socktype,
+ on_connected => sub { $sock = shift; },
+ on_resolve_error => sub { die "Test died early - resolve error - $_[-1]\n"; },
+ on_connect_error => sub { die "Test died early - connect error $_[0]() - $_[-1]\n"; },
+ );
+
+ wait_for { $sock };
+
+ can_ok( $sock, qw( peerhost peerport ) );
+ is_deeply( [ unpack_sockaddr_in $sock->peername ],
+ [ unpack_sockaddr_in $addr ], 'by host/service: $sock->getpeername is $addr' );
+
+ is( $sock->sockhost, $INADDR_LOOPBACK_HOST, '$sock->sockhost is INADDR_LOOPBACK_HOST' );
+
+ $listensock->accept; # Throw it away
+}
+
+SKIP: {
+ # Some OSes can't bind(2) locally to other addresses on 127./8
+ skip "Cannot bind to 127.0.0.2", 1 unless eval { IO::Socket::INET->new(
+ LocalHost => "127.0.0.2", LocalPort => 0
+ ) };
+
+ # Some can bind(2) but then cannot connect() to 127.0.0.1 from it
+ chomp($@), skip "Cannot connect to 127.0.0.1 from 127.0.0.2 - $@", 1 unless eval {
+ my $s = IO::Socket::INET->new(
+ LocalHost => "127.0.0.2", LocalPort => 0,
+ PeerHost => $listensock->sockhost, PeerPort => $listensock->sockport,
+ ) or die $@;
+ $listensock->accept; # Throw it away
+ $s->sockhost eq "127.0.0.2" or die "sockhost is not 127.0.0.2\n"; };
+
+ my $sock;
+
+ $loop->connect(
+ local_host => "127.0.0.2",
+ host => $listensock->sockhost,
+ service => $listensock->sockport,
+ socktype => $listensock->socktype,
+ on_connected => sub { $sock = shift; },
+ on_resolve_error => sub { die "Test died early - resolve error - $_[-1]\n"; },
+ on_connect_error => sub { die "Test died early - connect error $_[0]() - $_[-1]\n"; },
+ );
+
+ wait_for { $sock };
+
+ is( $sock->sockhost, "127.0.0.2", '$sock->sockhost is 127.0.0.2' );
+
+ $listensock->accept; # Throw it away
+ undef $sock; # This too
+}
+
+# Now try on_stream event
+{
+ my $stream;
+
+ $loop->connect(
+ host => $listensock->sockhost,
+ service => $listensock->sockport,
+ socktype => $listensock->socktype,
+ on_stream => sub { $stream = shift; },
+ on_resolve_error => sub { die "Test died early - resolve error - $_[-1]\n"; },
+ on_connect_error => sub { die "Test died early - connect error $_[0]() - $_[-1]\n"; },
+ );
+
+ wait_for { $stream };
+
+ isa_ok( $stream, "IO::Async::Stream", 'on_stream $stream isa IO::Async::Stream' );
+ my $sock = $stream->read_handle;
+ is_deeply( [ unpack_sockaddr_in $sock->peername ],
+ [ unpack_sockaddr_in $addr ], 'on_stream $sock->getpeername is $addr' );
+
+ $listensock->accept; # Throw it away
+}
+
+my $udpsock = IO::Socket::INET->new( LocalAddr => 'localhost', Protocol => 'udp' ) or
+ die "Cannot create udpsock - $!";
+
+{
+ my $future = $loop->connect(
+ handle => my $given_socket = IO::Async::Socket->new,
+ addr => { family => "inet", socktype => "dgram", addr => $udpsock->sockname },
+ );
+
+ isa_ok( $future, "Future", '$future for ->connect( handle socket )' );
+
+ wait_for { $future->is_ready };
+
+ my $socket = $future->get;
+ identical( $socket, $given_socket, '$future->get returns given Socket' );
+ is_deeply( [ unpack_sockaddr_in $socket->read_handle->peername ],
+ [ unpack_sockaddr_in $udpsock->sockname ], 'Returned $socket->read_handle->getpeername is $addr' );
+}
+
+# legacy callbacks
+{
+ my $sock;
+
+ $loop->connect(
+ addr => { family => "inet", socktype => "dgram", addr => $udpsock->sockname },
+ on_socket => sub { $sock = shift; },
+ on_connect_error => sub { die "Test died early - connect error $_[0]() - $_[-1]\n"; },
+ );
+
+ wait_for { $sock };
+
+ isa_ok( $sock, "IO::Async::Socket", 'on_socket $sock isa IO::Async::Socket' );
+ is_deeply( [ unpack_sockaddr_in $sock->read_handle->peername ],
+ [ unpack_sockaddr_in $udpsock->sockname ], 'on_socket $sock->read_handle->getpeername is $addr' );
+}
+
+SKIP: {
+ # Now try an address we know to be invalid - a UNIX socket that doesn't exist
+
+ socket( my $dummy, AF_UNIX, SOCK_STREAM, 0 ) or
+ skip "Cannot create AF_UNIX sockets - $!", 2;
+
+ my $error;
+
+ my $failop;
+ my $failerr;
+
+ $loop->connect(
+ addr => { family => "unix", socktype => "stream", path => "/some/path/we/know/breaks" },
+ on_connected => sub { die "Test died early - connect succeeded\n"; },
+ on_fail => sub { $failop = shift @_; $failerr = pop @_; },
+ on_connect_error => sub { $error = 1 },
+ );
+
+ wait_for { $error };
+
+ is( $failop, "connect", '$failop is connect' );
+ is( $failerr+0, ENOENT, '$failerr is ENOENT' );
+}
+
+SKIP: {
+ socket( my $dummy, AF_UNIX, SOCK_STREAM, 0 ) or
+ skip "Cannot create AF_UNIX sockets - $!", 2;
+
+ my $failop;
+ my $failerr;
+
+ my $future = $loop->connect(
+ addr => { family => "unix", socktype => "stream", path => "/some/path/we/know/breaks" },
+ on_fail => sub { $failop = shift @_; $failerr = pop @_; },
+ );
+
+ wait_for { $future->is_ready };
+
+ is( $failop, "connect", '$failop is connect' );
+ is( $failerr+0, ENOENT, '$failerr is ENOENT' );
+
+ ok( $future->is_failed, '$future failed' );
+ is( ( $future->failure )[2], "connect", '$future fail op is connect' );
+ is( ( $future->failure )[3]+0, ENOENT, '$future fail err is ENOENT' );
+}
+
+# UNIX sockets always connect(2) synchronously, meaning if they fail, the error
+# is available immediately. The above has therefore not properly tested
+# asynchronous connect(2) failures. INET sockets should do this.
+
+# First off we need a local socket that isn't listening - at lease one of the
+# first 100 is likely not to be
+
+my $port;
+my $failure;
+
+foreach ( 1 .. 100 ) {
+ IO::Socket::INET->new( PeerHost => "127.0.0.1", PeerPort => $_ ) and next;
+
+ $failure = "$!";
+ $port = $_;
+
+ last;
+}
+
+SKIP: {
+ skip "Cannot find an un-connect(2)able socket on 127.0.0.1", 2 unless defined $port;
+
+ my $failop;
+ my $failerr;
+
+ my @error;
+
+ $loop->connect(
+ addr => { family => "inet", socktype => "stream", port => $port, ip => "127.0.0.1" },
+ on_connected => sub { die "Test died early - connect succeeded\n"; },
+ on_fail => sub { $failop = shift @_; $failerr = pop @_; },
+ on_connect_error => sub { @error = @_; },
+ );
+
+ wait_for { @error };
+
+ is( $failop, "connect", '$failop is connect' );
+ is( "$failerr", $failure, "\$failerr is '$failure'" );
+
+ is( $error[0], "connect", '$error[0] is connect' );
+ is( "$error[1]", $failure, "\$error[1] is '$failure'" );
+}
+
+done_testing;
diff --git a/t/52loop-listen.t b/t/52loop-listen.t
new file mode 100644
index 0000000..5453630
--- /dev/null
+++ b/t/52loop-listen.t
@@ -0,0 +1,183 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+use Test::Identity;
+
+use IO::Socket::INET;
+
+use Socket qw( inet_ntoa unpack_sockaddr_in );
+
+use IO::Async::Loop;
+
+# Some odd locations like BSD jails might not like INADDR_LOOPBACK. We'll
+# establish a baseline first to test against
+my $INADDR_LOOPBACK = do {
+ my $localsock = IO::Socket::INET->new( LocalAddr => "localhost", Listen => 1 );
+ $localsock->sockaddr;
+};
+my $INADDR_LOOPBACK_HOST = inet_ntoa( $INADDR_LOOPBACK );
+if( $INADDR_LOOPBACK ne INADDR_LOOPBACK ) {
+ diag( "Testing with INADDR_LOOPBACK=$INADDR_LOOPBACK_HOST; this may be because of odd networking" );
+}
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+{
+ my $listensock = IO::Socket::INET->new(
+ LocalAddr => "localhost",
+ Type => SOCK_STREAM,
+ Listen => 1,
+ ) or die "Cannot socket() - $!";
+
+ my $newclient;
+
+ my $f = $loop->listen(
+ handle => $listensock,
+ on_accept => sub { $newclient = $_[0]; },
+ );
+
+ ok( $f->is_ready, '$loop->listen on handle ready synchronously' );
+
+ my $notifier = $f->get;
+ isa_ok( $notifier, "IO::Async::Notifier", 'synchronous on_notifier given a Notifier' );
+
+ identical( $notifier->loop, $loop, 'synchronous $notifier->loop is $loop' );
+
+ my $clientsock = IO::Socket::INET->new( Type => SOCK_STREAM )
+ or die "Cannot socket() - $!";
+
+ $clientsock->connect( $listensock->sockname ) or die "Cannot connect() - $!";
+
+ ok( defined $clientsock->peername, '$clientsock is connected' );
+
+ wait_for { defined $newclient };
+
+ is_deeply( [ unpack_sockaddr_in $newclient->peername ],
+ [ unpack_sockaddr_in $clientsock->sockname ], '$newclient peer is correct' );
+}
+
+{
+ my $listensock;
+ my $newclient;
+
+ my $f = $loop->listen(
+ family => "inet",
+ socktype => "stream",
+ service => "", # Ask the kernel to allocate a port for us
+ host => "localhost",
+
+ on_listen => sub { $listensock = $_[0]; },
+
+ on_accept => sub { $newclient = $_[0]; },
+ );
+
+ my $notifier = $f->get;
+
+ ok( defined $listensock->fileno, '$listensock has a fileno' );
+ # Not sure if it'll be an IO::Socket::INET or ::IP, but either way it should support these
+ can_ok( $listensock, qw( peerhost peerport ) );
+
+ isa_ok( $notifier, "IO::Async::Notifier", 'asynchronous on_notifier given a Notifier' );
+
+ identical( $notifier->loop, $loop, 'asynchronous $notifier->loop is $loop' );
+
+ my $listenaddr = $listensock->sockname;
+
+ ok( defined $listenaddr, '$listensock has address' );
+
+ my ( $listenport, $listen_inaddr ) = unpack_sockaddr_in( $listenaddr );
+
+ is( inet_ntoa( $listen_inaddr ), $INADDR_LOOPBACK_HOST, '$listenaddr is INADDR_LOOPBACK' );
+
+ my $clientsock = IO::Socket::INET->new( Type => SOCK_STREAM )
+ or die "Cannot socket() - $!";
+
+ $clientsock->connect( $listenaddr ) or die "Cannot connect() - $!";
+
+ is( (unpack_sockaddr_in( $clientsock->peername ))[0], $listenport, '$clientsock on the correct port' );
+
+ wait_for { defined $newclient };
+
+ can_ok( $newclient, qw( peerhost peerport ) );
+
+ is_deeply( [ unpack_sockaddr_in $newclient->peername ],
+ [ unpack_sockaddr_in $clientsock->sockname ], '$newclient peer is correct' );
+}
+
+# Now we want to test failure. It's hard to know in a test script what will
+# definitely fail, but it's likely we're either running as non-root, or the
+# machine has at least one of an SSH or a webserver running. In this case,
+# it's likely we'll fail to bind TCP port 22 or 80.
+
+my $badport;
+my $failure;
+foreach my $port ( 22, 80 ) {
+ IO::Socket::INET->new(
+ Type => SOCK_STREAM,
+ LocalHost => "localhost",
+ LocalPort => $port,
+ ReuseAddr => 1,
+ Listen => 1,
+ ) and next;
+
+ $badport = $port;
+ $failure = $!;
+ last;
+}
+
+SKIP: {
+ skip "No bind()-failing ports found", 6 unless defined $badport;
+
+ my $failop;
+ my $failerr;
+
+ my @error;
+
+ # We need to capture the Listener object before failure, so we can assert
+ # it gets removed from the Loop again afterwards
+ my $listener;
+ no warnings 'redefine';
+ my $add = IO::Async::Loop->can( "add" );
+ local *IO::Async::Loop::add = sub {
+ $listener = $_[1];
+ $add->( @_ );
+ };
+
+ $loop->listen(
+ family => "inet",
+ socktype => "stream",
+ host => "localhost",
+ service => $badport,
+
+ on_resolve_error => sub { die "Test died early - resolve error $_[0]\n"; },
+
+ on_listen => sub { die "Test died early - listen on port $badport actually succeeded\n"; },
+
+ on_accept => sub { "DUMMY" }, # really hope this doesn't happen ;)
+
+ on_fail => sub { $failop = shift; $failerr = pop; },
+ on_listen_error => sub { @error = @_; },
+ );
+
+ ok( defined $listener, 'Managed to capture listener being added to Loop' );
+
+ wait_for { @error };
+
+ is( $failop, "bind", '$failop is bind' );
+ is( "$failerr", $failure, "\$failerr is '$failure'" );
+
+ is( $error[0], "bind", '$error[0] is bind' );
+ is( "$error[1]", $failure, "\$error[1] is '$failure'" );
+
+ ok( defined $listener, '$listener defined after bind failure' );
+ ok( !$listener->loop, '$listener not in loop after bind failure' );
+}
+
+done_testing;
diff --git a/t/53loop-extend.t b/t/53loop-extend.t
new file mode 100644
index 0000000..82e7088
--- /dev/null
+++ b/t/53loop-extend.t
@@ -0,0 +1,103 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+use Test::Identity;
+
+use IO::Async::Loop;
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+# connect
+{
+ my %connectargs;
+ my $connect_future;
+ sub IO::Async::Loop::FOO_connect
+ {
+ my $self = shift;
+ %connectargs = @_;
+
+ identical( $self, $loop, 'FOO_connect invocant is $loop' );
+
+ return $connect_future = $loop->new_future;
+ }
+
+ my $sock;
+ my $f = $loop->connect(
+ extensions => [qw( FOO )],
+ some_param => "here",
+ on_connected => sub { $sock = shift },
+ );
+
+ is( ref delete $connectargs{on_connected}, "CODE", 'FOO_connect received on_connected continuation' );
+ is_deeply( \%connectargs,
+ { some_param => "here" },
+ 'FOO_connect received some_param and no others' );
+
+ identical( $f, $connect_future, 'FOO_connect returns Future object' );
+
+ $loop->connect(
+ extensions => [qw( FOO BAR )],
+ param1 => "one",
+ param2 => "two",
+ on_connected => sub { $sock = shift },
+ );
+
+ delete $connectargs{on_connected};
+ is_deeply( \%connectargs,
+ { extensions => [qw( BAR )],
+ param1 => "one",
+ param2 => "two" },
+ 'FOO_connect still receives other extensions' );
+}
+
+# listen
+{
+ my %listenargs;
+ my $listen_future;
+ sub IO::Async::Loop::FOO_listen
+ {
+ my $self = shift;
+ %listenargs = @_;
+
+ identical( $self, $loop, 'FOO_listen invocant is $loop' );
+
+ return $listen_future = $loop->new_future;
+ }
+
+ my $sock;
+ my $f = $loop->listen(
+ extensions => [qw( FOO )],
+ some_param => "here",
+ on_accept => sub { $sock = shift },
+ );
+
+ isa_ok( delete $listenargs{listener}, "IO::Async::Listener", '$listenargs{listener}' );
+ is_deeply( \%listenargs,
+ { some_param => "here" },
+ 'FOO_listen received some_param and no others' );
+
+ identical( $f, $listen_future, 'FOO_listen returns Future object' );
+
+ $loop->listen(
+ extensions => [qw( FOO BAR )],
+ param1 => "one",
+ param2 => "two",
+ on_accept => sub { $sock = shift },
+ );
+
+ delete $listenargs{listener};
+ is_deeply( \%listenargs,
+ { extensions => [qw( BAR )],
+ param1 => "one",
+ param2 => "two" },
+ 'FOO_listen still receives other extensions' );
+}
+
+done_testing;
diff --git a/t/60protocol.t b/t/60protocol.t
new file mode 100644
index 0000000..493931e
--- /dev/null
+++ b/t/60protocol.t
@@ -0,0 +1,146 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+use Test::Identity;
+use Test::Refcount;
+
+use IO::Async::Loop;
+
+use IO::Async::OS;
+
+use IO::Async::Handle;
+use IO::Async::Protocol;
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+my ( $S1, $S2 ) = IO::Async::OS->socketpair or die "Cannot create socket pair - $!";
+
+# Need sockets in nonblocking mode
+$S1->blocking( 0 );
+$S2->blocking( 0 );
+
+my $handle = IO::Async::Handle->new(
+ handle => $S1,
+ on_read_ready => sub {},
+ on_write_ready => sub {},
+);
+
+my @setup_args;
+my @teardown_args;
+my $readready;
+my $writeready;
+
+my $proto = TestProtocol->new;
+
+ok( defined $proto, '$proto defined' );
+isa_ok( $proto, "IO::Async::Protocol", '$proto isa IO::Async::Protocol' );
+
+is_oneref( $proto, '$proto has refcount 1 initially' );
+
+$proto->configure( transport => $handle );
+
+identical( $proto->transport, $handle, '$proto->transport' );
+
+is( scalar @setup_args, 1, '@setup_args after configure transport' );
+identical( $setup_args[0], $handle, '$setup_args[0] after configure transport');
+
+undef @setup_args;
+
+is_oneref( $proto, '$proto has refcount 1 after configure transport' );
+# lexical $handle, $proto->{transport}, $proto->{children} == 3
+is_refcount( $handle, 3, '$handle has refcount 3 after proto configure transport' );
+
+$loop->add( $proto );
+
+is_refcount( $proto, 2, '$proto has refcount 2 after adding to Loop' );
+is_refcount( $handle, 4, '$handle has refcount 4 after adding proto to Loop' );
+
+$S2->syswrite( "hello\n" );
+
+wait_for { $readready };
+
+is( $readready, 1, '$readready after wait' );
+
+# Just to shut poll/select/etc... up
+$S1->sysread( my $dummy, 8192 );
+
+my $newhandle = IO::Async::Handle->new(
+ handle => $S1,
+ on_read_ready => sub {},
+ on_write_ready => sub {},
+);
+
+$proto->configure( transport => $newhandle );
+
+identical( $proto->transport, $newhandle, '$proto->transport after reconfigure' );
+
+is( scalar @teardown_args, 1, '@teardown_args after reconfigure transport' );
+identical( $teardown_args[0], $handle, '$teardown_args[0] after reconfigure transport');
+
+is( scalar @setup_args, 1, '@setup_args after reconfigure transport' );
+identical( $setup_args[0], $newhandle, '$setup_args[0] after reconfigure transport');
+
+undef @teardown_args;
+undef @setup_args;
+
+is_oneref( $handle, '$handle has refcount 1 after reconfigure' );
+
+my $closed = 0;
+$proto->configure(
+ on_closed => sub { $closed++ },
+);
+
+$proto->transport->close;
+
+wait_for { $closed };
+
+is( $closed, 1, '$closed after stream close' );
+
+is( $proto->transport, undef, '$proto->transport is undef after close' );
+
+is_refcount( $proto, 2, '$proto has refcount 2 before removal from Loop' );
+
+$loop->remove( $proto );
+
+is_oneref( $proto, '$proto has refcount 1 before EOF' );
+
+done_testing;
+
+package TestProtocol;
+use base qw( IO::Async::Protocol );
+
+sub setup_transport
+{
+ my $self = shift;
+ @setup_args = @_;
+
+ my ( $transport ) = @_;
+
+ $self->SUPER::setup_transport( $transport );
+
+ $transport->configure(
+ on_read_ready => sub { $readready = 1 },
+ on_write_ready => sub { $writeready = 1 },
+ );
+}
+
+sub teardown_transport
+{
+ my $self = shift;
+ @teardown_args = @_;
+
+ my ( $transport ) = @_;
+ $transport->configure(
+ on_read_ready => sub {},
+ on_write_ready => sub {},
+ );
+
+ $self->SUPER::teardown_transport( $transport );
+}
diff --git a/t/61protocol-stream.t b/t/61protocol-stream.t
new file mode 100644
index 0000000..da5832a
--- /dev/null
+++ b/t/61protocol-stream.t
@@ -0,0 +1,245 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+use Test::Refcount;
+
+use IO::Async::Loop;
+
+use IO::Async::OS;
+
+use IO::Async::Stream;
+use IO::Async::Protocol::Stream;
+
+use IO::Socket::INET;
+use Socket qw( SOCK_STREAM );
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+{
+ my ( $S1, $S2 ) = IO::Async::OS->socketpair or die "Cannot create socket pair - $!";
+
+# Need sockets in nonblocking mode
+ $S1->blocking( 0 );
+ $S2->blocking( 0 );
+
+ my @lines;
+
+ my $streamproto = IO::Async::Protocol::Stream->new(
+ transport => IO::Async::Stream->new( handle => $S1 ),
+ on_read => sub {
+ my $self = shift;
+ my ( $buffref, $eof ) = @_;
+
+ push @lines, $1 while $$buffref =~ s/^(.*\n)//;
+ return 0;
+ },
+ );
+
+ ok( defined $streamproto, '$streamproto defined' );
+ isa_ok( $streamproto, "IO::Async::Protocol::Stream", '$streamproto isa IO::Async::Protocol::Stream' );
+
+ is_oneref( $streamproto, '$streamproto has refcount 1 initially' );
+
+ $loop->add( $streamproto );
+
+ is_refcount( $streamproto, 2, '$streamproto has refcount 2 after adding to Loop' );
+
+ $S2->syswrite( "message\n" );
+
+ is_deeply( \@lines, [], '@lines before wait' );
+
+ wait_for { scalar @lines };
+
+ is_deeply( \@lines, [ "message\n" ], '@lines after wait' );
+
+ undef @lines;
+ my @new_lines;
+ $streamproto->configure(
+ on_read => sub {
+ my $self = shift;
+ my ( $buffref, $eof ) = @_;
+
+ push @new_lines, $1 while $$buffref =~ s/^(.*\n)//;
+ return 0;
+ },
+ );
+
+ $S2->syswrite( "new\nlines\n" );
+
+ wait_for { scalar @new_lines };
+
+ is( scalar @lines, 0, '@lines still empty after on_read replace' );
+ is_deeply( \@new_lines, [ "new\n", "lines\n" ], '@new_lines after on_read replace' );
+
+ $streamproto->write( "response\n" );
+
+ my $response = "";
+ wait_for_stream { $response =~ m/\n/ } $S2 => $response;
+
+ is( $response, "response\n", 'response written by protocol' );
+
+ my $done;
+ my $flushed;
+
+ $streamproto->write(
+ sub {
+ is( $_[0], $streamproto, 'writersub $_[0] is $streamproto' );
+ return $done++ ? undef : "a lazy message\n";
+ },
+ on_flush => sub {
+ is( $_[0], $streamproto, 'on_flush $_[0] is $streamproto' );
+ $flushed = 1;
+ },
+ );
+
+ wait_for { $flushed };
+
+ $response = "";
+ wait_for_stream { $response =~ m/\n/ } $S2 => $response;
+
+ is( $response, "a lazy message\n", 'response written by protocol writersub' );
+
+ my $closed = 0;
+ $streamproto->configure(
+ on_closed => sub { $closed++ },
+ );
+
+ $S2->close;
+
+ wait_for { $closed };
+
+ is( $closed, 1, '$closed after stream close' );
+
+ is_refcount( $streamproto, 2, '$streamproto has refcount 2 before removing from Loop' );
+
+ $loop->remove( $streamproto );
+
+ is_oneref( $streamproto, '$streamproto refcount 1 finally' );
+}
+
+my @sub_lines;
+
+{
+ my ( $S1, $S2 ) = IO::Async::OS->socketpair or die "Cannot create socket pair - $!";
+
+ # Need sockets in nonblocking mode
+ $S1->blocking( 0 );
+ $S2->blocking( 0 );
+
+ my $streamproto = TestProtocol::Stream->new(
+ transport => IO::Async::Stream->new( handle => $S1 ),
+ );
+
+ ok( defined $streamproto, 'subclass $streamproto defined' );
+ isa_ok( $streamproto, "IO::Async::Protocol::Stream", '$streamproto isa IO::Async::Protocol::Stream' );
+
+ is_oneref( $streamproto, 'subclass $streamproto has refcount 1 initially' );
+
+ $loop->add( $streamproto );
+
+ is_refcount( $streamproto, 2, 'subclass $streamproto has refcount 2 after adding to Loop' );
+
+ $S2->syswrite( "message\n" );
+
+ is_deeply( \@sub_lines, [], '@sub_lines before wait' );
+
+ wait_for { scalar @sub_lines };
+
+ is_deeply( \@sub_lines, [ "message\n" ], '@sub_lines after wait' );
+
+ $loop->remove( $streamproto );
+}
+
+{
+ my ( $S1, $S2 ) = IO::Async::OS->socketpair or die "Cannot create socket pair - $!";
+
+ # Need sockets in nonblocking mode
+ $S1->blocking( 0 );
+ $S2->blocking( 0 );
+
+ my $serversock = IO::Socket::INET->new(
+ Type => SOCK_STREAM,
+ LocalHost => "localhost",
+ LocalPort => 0,
+ Listen => 1,
+ ) or die "Cannot create server socket - $!";
+
+ my @lines;
+ my $streamproto = IO::Async::Protocol::Stream->new(
+ on_read => sub {
+ my $self = shift;
+ my ( $buffref, $eof ) = @_;
+ push @lines, $1 while $$buffref =~ s/^(.*\n)//;
+ return 0;
+ }
+ );
+
+ $loop->add( $streamproto );
+
+ my $connected = 0;
+
+ $streamproto->connect(
+ host => $serversock->sockhost,
+ service => $serversock->sockport,
+ family => $serversock->sockdomain,
+
+ on_connected => sub { $connected++ },
+
+ on_connect_error => sub { die "Test failed early - $_[-1]" },
+ on_resolve_error => sub { die "Test failed early - $_[-1]" },
+ );
+
+ wait_for { $connected };
+
+ my $clientsock = $serversock->accept;
+
+ is( $streamproto->transport->read_handle->peerport,
+ $serversock->sockport,
+ 'Protocol is connected to server socket port' );
+
+ $clientsock->syswrite( "A message\n" );
+
+ undef @lines;
+
+ wait_for { @lines };
+
+ is( $lines[0], "A message\n", 'Protocol transport works' );
+}
+
+{
+ my $read_eof;
+ my $write_eof;
+ my $streamproto = IO::Async::Protocol::Stream->new(
+ on_read_eof => sub { $read_eof++ },
+ on_write_eof => sub { $write_eof++ },
+ );
+
+ $streamproto->configure( transport => my $stream = IO::Async::Stream->new );
+
+ $stream->invoke_event( on_read_eof => );
+ is( $read_eof, 1, '$read_eof after on_read_eof' );
+
+ $stream->invoke_event( on_write_eof => );
+ is( $write_eof, 1, '$write_eof after on_write_eof' );
+}
+
+done_testing;
+
+package TestProtocol::Stream;
+use base qw( IO::Async::Protocol::Stream );
+
+sub on_read
+{
+ my $self = shift;
+ my ( $buffref, $eof ) = @_;
+
+ push @sub_lines, $1 while $$buffref =~ s/^(.*\n)//;
+ return 0;
+}
diff --git a/t/62protocol-linestream.t b/t/62protocol-linestream.t
new file mode 100644
index 0000000..2acab97
--- /dev/null
+++ b/t/62protocol-linestream.t
@@ -0,0 +1,118 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+use Test::Refcount;
+
+use IO::Async::Loop;
+
+use IO::Async::OS;
+
+use IO::Async::Protocol::LineStream;
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+my ( $S1, $S2 ) = IO::Async::OS->socketpair or die "Cannot create socket pair - $!";
+
+# Need sockets in nonblocking mode
+$S1->blocking( 0 );
+$S2->blocking( 0 );
+
+my @lines;
+
+my $linestreamproto = IO::Async::Protocol::LineStream->new(
+ handle => $S1,
+ on_read_line => sub {
+ my $self = shift;
+
+ push @lines, $_[0];
+ },
+);
+
+ok( defined $linestreamproto, '$linestreamproto defined' );
+isa_ok( $linestreamproto, "IO::Async::Protocol::LineStream", '$linestreamproto isa IO::Async::Protocol::LineStream' );
+
+is_oneref( $linestreamproto, '$linestreamproto has refcount 1 initially' );
+
+$loop->add( $linestreamproto );
+
+is_refcount( $linestreamproto, 2, '$linestreamproto has refcount 2 after adding to Loop' );
+
+$S2->syswrite( "message\r\n" );
+
+is_deeply( \@lines, [], '@lines before wait' );
+
+wait_for { scalar @lines };
+
+is_deeply( \@lines, [ "message" ], '@lines after wait' );
+
+undef @lines;
+my @new_lines;
+$linestreamproto->configure(
+ on_read_line => sub {
+ my $self = shift;
+
+ push @new_lines, $_[0];
+ },
+);
+
+$S2->syswrite( "new\r\nlines\r\n" );
+
+wait_for { scalar @new_lines };
+
+is( scalar @lines, 0, '@lines still empty after on_read replace' );
+is_deeply( \@new_lines, [ "new", "lines" ], '@new_lines after on_read replace' );
+
+$linestreamproto->write_line( "response" );
+
+my $response = "";
+wait_for_stream { $response =~ m/\r\n/ } $S2 => $response;
+
+is( $response, "response\r\n", 'response written by protocol' );
+
+my @sub_lines;
+
+$linestreamproto = TestProtocol::Stream->new(
+ handle => $S1,
+);
+
+ok( defined $linestreamproto, 'subclass $linestreamproto defined' );
+isa_ok( $linestreamproto, "IO::Async::Protocol::LineStream", '$linestreamproto isa IO::Async::Protocol::LineStream' );
+
+is_oneref( $linestreamproto, 'subclass $linestreamproto has refcount 1 initially' );
+
+$loop->add( $linestreamproto );
+
+is_refcount( $linestreamproto, 2, 'subclass $linestreamproto has refcount 2 after adding to Loop' );
+
+$S2->syswrite( "message\r\n" );
+
+is_deeply( \@sub_lines, [], '@sub_lines before wait' );
+
+wait_for { scalar @sub_lines };
+
+is_deeply( \@sub_lines, [ "message" ], '@sub_lines after wait' );
+
+undef @lines;
+
+$loop->remove( $linestreamproto );
+
+undef $linestreamproto;
+
+done_testing;
+
+package TestProtocol::Stream;
+use base qw( IO::Async::Protocol::LineStream );
+
+sub on_read_line
+{
+ my $self = shift;
+
+ push @sub_lines, $_[0];
+}
diff --git a/t/63handle-connect.t b/t/63handle-connect.t
new file mode 100644
index 0000000..1318923
--- /dev/null
+++ b/t/63handle-connect.t
@@ -0,0 +1,84 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+
+use IO::Async::Loop;
+
+use IO::Async::Handle;
+
+use IO::Async::OS;
+
+use IO::Socket::INET;
+use Socket qw( SOCK_STREAM );
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+# Try connect(2)ing to a socket we've just created
+my $listensock = IO::Socket::INET->new(
+ Type => SOCK_STREAM,
+ LocalAddr => 'localhost',
+ LocalPort => 0,
+ Listen => 1
+) or die "Cannot create listensock - $!";
+
+my $addr = $listensock->sockname;
+
+# ->connect to plain addr
+{
+ my $handle = IO::Async::Handle->new(
+ on_read_ready => sub {},
+ on_write_ready => sub {},
+ );
+
+ $loop->add( $handle );
+
+ my $f = $handle->connect( addr => [ 'inet', 'stream', 0, $addr ] );
+
+ ok( defined $f, '$handle->connect Future defined' );
+
+ wait_for { $f->is_ready };
+ $f->is_failed and $f->get;
+
+ ok( defined $handle->read_handle, '$handle->read_handle defined after ->connect addr' );
+ is( $handle->read_handle->peerport, $listensock->sockport, '$handle->read_handle->peerport after ->connect addr' );
+
+ $listensock->accept; # drop it
+
+ $loop->remove( $handle );
+}
+
+# ->connect to host/service
+{
+ my $handle = IO::Async::Handle->new(
+ on_read_ready => sub {},
+ on_write_ready => sub {},
+ );
+
+ $loop->add( $handle );
+
+ my $f = $handle->connect(
+ family => "inet",
+ socktype => "stream",
+ host => $listensock->sockhost,
+ service => $listensock->sockport,
+ );
+
+ wait_for { $f->is_ready };
+ $f->is_failed and $f->get;
+
+ ok( defined $handle->read_handle, '$handle->read_handle defined after ->connect host/service' );
+ is( $handle->read_handle->peerport, $listensock->sockport, '$handle->read_handle->peerport after ->connect host/service' );
+
+ $listensock->accept; # drop it
+
+ $loop->remove( $handle );
+}
+
+done_testing;
diff --git a/t/64handle-bind.t b/t/64handle-bind.t
new file mode 100644
index 0000000..772f9e6
--- /dev/null
+++ b/t/64handle-bind.t
@@ -0,0 +1,36 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use IO::Async::Test;
+
+use Test::More;
+
+use IO::Async::Loop;
+
+use IO::Async::Handle;
+
+my $loop = IO::Async::Loop->new_builtin;
+
+testing_loop( $loop );
+
+# ->bind a UDP service
+{
+ my $recv_count;
+
+ my $receiver = IO::Async::Handle->new(
+ on_read_ready => sub { $recv_count++ },
+ on_write_ready => sub { },
+ );
+ $loop->add( $receiver );
+
+ $receiver->bind(
+ service => "0",
+ socktype => "dgram",
+ )->get;
+
+ ok( $receiver->read_handle->sockport, '$receiver bound to a read handle' );
+}
+
+done_testing;
diff --git a/t/99pod.t b/t/99pod.t
new file mode 100644
index 0000000..eb319fb
--- /dev/null
+++ b/t/99pod.t
@@ -0,0 +1,11 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+
+eval "use Test::Pod 1.00";
+plan skip_all => "Test::Pod 1.00 required for testing POD" if $@;
+
+all_pod_files_ok();
diff --git a/t/StupidLoop.pm b/t/StupidLoop.pm
new file mode 100644
index 0000000..0b4fc9a
--- /dev/null
+++ b/t/StupidLoop.pm
@@ -0,0 +1,8 @@
+package t::StupidLoop;
+
+use strict;
+use base qw( IO::Async::Loop );
+
+sub new { return bless {}, shift; }
+
+1;
diff --git a/t/TimeAbout.pm b/t/TimeAbout.pm
new file mode 100644
index 0000000..86b3d1f
--- /dev/null
+++ b/t/TimeAbout.pm
@@ -0,0 +1,31 @@
+package t::TimeAbout;
+
+use Test::More;
+use Time::HiRes qw( time );
+
+use constant AUT => $ENV{TEST_QUICK_TIMERS} ? 0.1 : 1;
+
+use Exporter 'import';
+our @EXPORT = qw( time_about );
+
+# Kindof like Test::Timer only we use Time::HiRes
+# We'll be quite lenient on the time taken, in case of heavy test machine load
+sub time_about
+{
+ my ( $code, $target, $name ) = @_;
+
+ my $lower = $target*0.75;
+ my $upper = $target*1.5 + 1;
+
+ my $now = time;
+ $code->();
+ my $took = (time - $now) / AUT;
+
+ cmp_ok( $took, '>', $lower, "$name took at least $lower" );
+ cmp_ok( $took, '<', $upper * 3, "$name took no more than $upper" );
+ if( $took > $upper and $took <= $upper * 3 ) {
+ diag( "$name took longer than $upper - this may just be an indication of a busy testing machine rather than a bug" );
+ }
+}
+
+0x55AA;