Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 4 additions & 13 deletions .github/workflows/linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,15 @@ jobs:
fail-fast: false
matrix:
cip_tag:
- "5.41"
- "5.40"
- "5.38"
- "5.36"
- "5.34"
- "5.32"
- "5.30"
- "5.28"
- "5.26"
- "5.24"
- "5.22"
- "5.20"
- "5.45"
- "5.44"
- "5.42"

env:
CIP_TAG: ${{ matrix.cip_tag }}

steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7

- name: Bootstrap CIP
run: |
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
IO-Async-Open3-Simple-*
/.build/
*.swp
*.bak
80 changes: 80 additions & 0 deletions .perltidyrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Initialized via perltidy -b -w -dop | grep -v dump-options > .perltidyrc
--add-newlines
--add-semicolons
--add-whitespace
--backup-and-modify-in-place
--backup-file-extension="bak"
--blank-lines-before-packages=1
--blank-lines-before-subs=1
--blanks-before-blocks
--noblanks-before-comments
--block-brace-tightness=0
--block-brace-vertical-tightness=0
--nobrace-left-and-indent
--brace-tightness=1
--brace-vertical-tightness=0
--brace-vertical-tightness-closing=0
--break-at-old-attribute-breakpoints
--break-at-old-keyword-breakpoints
--break-at-old-logical-breakpoints
--break-at-old-method-breakpoints
--break-at-old-ternary-breakpoints
--nocheck-syntax
--closing-brace-indentation=0
--closing-paren-indentation=0
--closing-side-comment-else-flag=0
--closing-side-comment-interval=6
--closing-side-comment-maximum-text=20
--closing-side-comments-balanced
--closing-square-bracket-indentation=0
--comma-arrow-breakpoints=5
--continuation-indentation=2
--cuddled-else
--default-tabsize=8
--delete-old-newlines
--nodelete-old-whitespace
--delete-semicolons
--format="tidy"
--format-skipping
--fuzzy-line-length
--hanging-side-comments
--nohtml
--html-entities
--html-table-of-contents
--indent-block-comments
--indent-columns=4
--iterations=1
--keep-old-blank-lines=1
--nologfile
--long-block-line-count=8
--look-for-autoloader
--look-for-selfloader
--maximum-consecutive-blank-lines=1
--maximum-fields-per-table=0
--maximum-line-length=120
--memoize
--minimum-space-to-comment=4
--outdent-labels
--outdent-long-comments
--outdent-long-quotes
--paren-tightness=1
--paren-vertical-tightness=0
--paren-vertical-tightness-closing=0
--pass-version-line
--perl-syntax-check-flags="-c -T"
--pod2html
--noquiet
--recombine
--short-concatenation-item-length=8
--noshow-options
--space-for-semicolon
--square-bracket-tightness=1
--square-bracket-vertical-tightness=0
--square-bracket-vertical-tightness-closing=0
--static-block-comments
--nostatic-side-comments
--notabs
--trim-qw
--valign
--warning-output
--character-encoding=utf8
281 changes: 281 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
# IO::Async::Open3::Simple ![static](https://github.com/uperl/IO-Async-Open3-Simple/workflows/static/badge.svg) ![linux](https://github.com/uperl/IO-Async-Open3-Simple/workflows/linux/badge.svg)

Interface to open3 under IO::Async

# SYNOPSIS

```perl
use v5.42;
use IO::Async::Loop;
use IO::Async::Open3::Simple;

my $loop = IO::Async::Loop->new;
my $done = $loop->new_future;

my $ipc = IO::Async::Open3::Simple->new(
on_start => sub {
my $proc = shift; # isa IO::Async::Open3::Simple::Process
my $program = shift; # string
my @args = @_; # list of arguments
say 'child PID: ', $proc->pid;
},
on_stdout => sub {
my $proc = shift; # isa IO::Async::Open3::Simple::Process
my $line = shift; # string
say 'out: ', $line;
},
on_stderr => sub {
my $proc = shift; # isa IO::Async::Open3::Simple::Process
my $line = shift; # string
say 'err: ', $line;
},
on_exit => sub {
my $proc = shift; # isa IO::Async::Open3::Simple::Process
my $exit_value = shift; # integer
my $signal = shift; # integer
say 'exit value: ', $exit_value;
say 'signal: ', $signal;
$done->done;
},
on_error => sub {
my $error = shift; # the exception thrown by IPC::Open3::open3
my $program = shift; # string
my @args = @_; # list of arguments
warn "error: $error";
$done->done;
},
);

$ipc->run('echo', 'hello there');
$done->get;
```

# DESCRIPTION

This module provides an interface to open3 while running under [IO::Async](https://metacpan.org/pod/IO::Async)
that delivers data from stdout and stderr as lines are written by the
subprocess. The interface is reminiscent of [IPC::Open3::Simple](https://metacpan.org/pod/IPC::Open3::Simple),
although this module does provide a somewhat different API, so it
cannot be used as a drop in replacement for that module.

It is intended as a drop in replacement for [AnyEvent::Open3::Simple](https://metacpan.org/pod/AnyEvent::Open3::Simple),
for code that would rather use [IO::Async](https://metacpan.org/pod/IO::Async) as its event loop. Aside
from the `AnyEvent` specific parts (the `implementation` attribute
and the `ANYEVENT_OPEN3_SIMPLE` environment variable, neither of which
apply here), the API is the same.

[IO::Async](https://metacpan.org/pod/IO::Async) comes with a robust interface to do the same thing as this
module: [IO::Async::Process](https://metacpan.org/pod/IO::Async::Process), which you more than likely want to use
instead. This module is primarily intended for applications that are
already using [AnyEvent::Open3::Simple](https://metacpan.org/pod/AnyEvent::Open3::Simple) and only want to change the
underlying event loop.

# CONSTRUCTOR

Constructor takes a hash or hashref of event callbacks and attributes.
Event callbacks have an `on_` prefix, attributes do not.

## ATTRIBUTES

- loop

The [IO::Async::Loop](https://metacpan.org/pod/IO::Async::Loop) to use. If not provided the shared loop returned
by `IO::Async::Loop->new` is used, which is almost always what you
want.

- implementation

Accepted and ignored for compatibility with [AnyEvent::Open3::Simple](https://metacpan.org/pod/AnyEvent::Open3::Simple).
Under [IO::Async](https://metacpan.org/pod/IO::Async) there is only one implementation: an [IO::Async::Stream](https://metacpan.org/pod/IO::Async::Stream)
for each of stdout and stderr, and `$loop->watch_process` to detect
process termination.

## EVENTS

These events will be triggered by the subprocess when the run method is
called. Each event callback (except `on_error`) gets passed in an
instance of [IO::Async::Open3::Simple::Process](https://metacpan.org/pod/IO::Async::Open3::Simple::Process) as its first argument
which can be used to get the PID of the subprocess, or to write to it.
`on_error` does not get a process object because it indicates an error in
the creation of the process.

Not all of these events will fire depending on the execution of the
child process. In the very least exactly one of `on_start` or `on_error`
will be called.

- `on_start` ($proc, $program, @arguments)

Called after the process is created, but before the run method returns
(that is, it does not wait to re-enter the event loop first).

This event also gets the program name and arguments passed into the
[run](https://metacpan.org/pod/IO::Async::Open3::Simple#run) method.

- `on_error` ($error, $program, @arguments)

Called when there is an execution error, for example, if you ask
to run a program that does not exist. No process is passed in
because the process failed to create. The error passed in is
the error thrown by [IPC::Open3](https://metacpan.org/pod/IPC::Open3) (typically a string which begins
with "open3: ...").

In some environments open3 is unable to detect exec errors in the
child, so you may not be able to rely on this event. It does
seem to work consistently on Perl 5.14 or better though.

Different environments have different ways of handling it when
you ask to run a program that doesn't exist. On Linux and Cygwin,
this will raise an `on_error` event, on `MSWin32` it will
not trigger a `on_error` and instead cause a normal exit
with a exit value of 1.

This event also gets the program name and arguments passed into the
[run](https://metacpan.org/pod/IO::Async::Open3::Simple#run) method.

- `on_stdout` ($proc, $line)

Called on every line printed to stdout by the child process.

- `on_stderr` ($proc, $line)

Called on every line printed to stderr by the child process.

- `on_exit` ($proc, $exit\_value, $signal)

Called when the processes completes, either because it called exit,
or if it was killed by a signal.

- `on_success` ($proc)

Called when the process returns zero exit value and is not terminated by a signal.

- `on_signal` ($proc, $signal)

Called when the processes is terminated by a signal.

- `on_fail` ($proc, $exit\_value)

Called when the process returns a non-zero exit value.

# METHODS

## run

```perl
$ipc->run($program, @arguments);
$ipc->run($program, @arguments, \$stdin);
$ipc->run($program, @arguments, \@stdin);
$ipc->run($program, @arguments, sub {...});
$ipc->run($program, @arguments, \$stdin, sub {...});
$ipc->run($program, @arguments, \@stdin, sub {...});
```

Start the given program with the given arguments. Returns
immediately (it returns the [IO::Async::Open3::Simple](https://metacpan.org/pod/IO::Async::Open3::Simple) instance).
Any events that have been specified in the constructor (except for
`on_start`) will not be called until the process re-enters the
event loop.

You may optionally provide the full content of standard input
as a string reference or list reference as the last argument
(or second to last if you are providing a callback below).
If provided as a list reference, it will be joined by new lines
in whatever format is native to your Perl. Currently on
(non cygwin) Windows (Strawberry, ActiveState) this is the only
way to provide standard input to the subprocess.

Do not mix the use of passing standard input to [run](https://metacpan.org/pod/IO::Async::Open3::Simple#run)
and [IO::Async::Open3::Simple::Process#print](https://metacpan.org/pod/IO::Async::Open3::Simple::Process#print) or [IO::Async::Open3::Simple::Process#say](https://metacpan.org/pod/IO::Async::Open3::Simple::Process#say),
otherwise bad things may happen.

You may provide a callback as the last argument which is called before
`on_start`, and takes the process object as its only argument. For
example:

```perl
foreach my $i (1..10)
{
$ipc->run($prog, @args, \$stdin, sub {
my($proc) = @_;
$proc->user({ iteration => $i });
});
}
```

This is useful for making data accessible to `$ipc` object's callbacks that may
be out of scope otherwise.

# CAVEATS

There are some traps for the unwary relating to buffers and deadlocks,
[IPC::Open3](https://metacpan.org/pod/IPC::Open3) is recommended reading.

Unlike [AnyEvent::Open3::Simple](https://metacpan.org/pod/AnyEvent::Open3::Simple), this module waits for the child's
stdout and stderr pipes to reach end of file (in addition to the child
process being reaped) before firing `on_exit`. This guarantees that
every line of output is delivered before `on_exit`, but a grandchild
process which inherits and holds open the pipes can delay the event.

If you register a call back for `on_exit`, but not `on_error` then
use a [Future](https://metacpan.org/pod/Future) (or condition variable, or `$loop->run` / `$loop->stop`)
to wait for the process to complete as in this:

```perl
my $done = $loop->new_future;
my $ipc = IO::Async::Open3::Simple->new(
on_exit => sub { $done->done },
);
$ipc->run('command_not_found');
$done->get;
```

You might be waiting forever if there is an error starting the
process (if for example you give it a bad command). To handle
this situation you might fail the Future in the event of error:

```perl
my $done = $loop->new_future;
my $ipc = IO::Async::Open3::Simple->new(
on_exit => sub { $done->done },
on_error => sub {
my $error = shift;
$done->fail($error);
},
);
$ipc->run('command_not_found');
$done->get;
```

This will cause the `get` to die, printing a useful diagnostic
if the exception isn't caught somewhere else.

Writing to a subprocesses stdin with [IO::Async::Open3::Simple::Process#print](https://metacpan.org/pod/IO::Async::Open3::Simple::Process#print)
or [IO::Async::Open3::Simple::Process#say](https://metacpan.org/pod/IO::Async::Open3::Simple::Process#say) is unsupported on Microsoft
Windows (it does work under Cygwin though).

# SEE ALSO

- [IO::Async::Open3::Simple::Process](https://metacpan.org/pod/IO::Async::Open3::Simple::Process)

Represents a process being run by this module, typically passed
into the callbacks.

- [AnyEvent::Open3::Simple](https://metacpan.org/pod/AnyEvent::Open3::Simple)

The module this one is based on, for use with [AnyEvent](https://metacpan.org/pod/AnyEvent) instead
of [IO::Async](https://metacpan.org/pod/IO::Async).

- [IO::Async::Process](https://metacpan.org/pod/IO::Async::Process)

Alternative to this module included with [IO::Async](https://metacpan.org/pod/IO::Async).

# AUTHOR

Graham Ollis <plicease@cpan.org>

# COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Graham Ollis.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
Loading
Loading