POE::Wheel::Run(3pm)

πŸ’– POE::Wheel::Run πŸ’–

okay bestie, gather round, because this one is POE::Wheel::Run from the User Contributed Perl Documentation collection, and it is the way to run blocking code and whole programs in subprocesses, portably, without your whole event loop throwing a tantrum. πŸ¦‹


✨ NAME ✨

POE::Wheel::Run β€” portably run blocking code and programs in subprocesses. that's it, that's the whole vibe. πŸ‘‘


🌸 SYNOPSIS 🌸

no notes, just look at her:

#!/usr/bin/perl

use warnings;
use strict;

use POE qw( Wheel::Run );

POE::Session->create(
  inline_states => {
    _start           => \&on_start,
    got_child_stdout => \&on_child_stdout,
    got_child_stderr => \&on_child_stderr,
    got_child_close  => \&on_child_close,
    got_child_signal => \&on_child_signal,
  }
);

POE::Kernel->run();
exit 0;

sub on_start {
  my $child = POE::Wheel::Run->new(
    Program => [ "/bin/ls", "-1", "/" ],
    StdoutEvent  => "got_child_stdout",
    StderrEvent  => "got_child_stderr",
    CloseEvent   => "got_child_close",
  );

  $_[KERNEL]->sig_child($child->PID, "got_child_signal");

  # Wheel events include the wheel's ID.
  $_[HEAP]{children_by_wid}{$child->ID} = $child;

  # Signal events include the process ID.
  $_[HEAP]{children_by_pid}{$child->PID} = $child;

  print(
    "Child pid ", $child->PID,
    " started as wheel ", $child->ID, ".\n"
  );
}

# Wheel event, including the wheel's ID.
sub on_child_stdout {
  my ($stdout_line, $wheel_id) = @_[ARG0, ARG1];
  my $child = $_[HEAP]{children_by_wid}{$wheel_id};
  print "pid ", $child->PID, " STDOUT: $stdout_line\n";
}

# Wheel event, including the wheel's ID.
sub on_child_stderr {
  my ($stderr_line, $wheel_id) = @_[ARG0, ARG1];
  my $child = $_[HEAP]{children_by_wid}{$wheel_id};
  print "pid ", $child->PID, " STDERR: $stderr_line\n";
}

# Wheel event, including the wheel's ID.
sub on_child_close {
  my $wheel_id = $_[ARG0];
  my $child = delete $_[HEAP]{children_by_wid}{$wheel_id};

  # May have been reaped by on_child_signal().
  unless (defined $child) {
    print "wid $wheel_id closed all pipes.\n";
    return;
  }

  print "pid ", $child->PID, " closed all pipes.\n";
  delete $_[HEAP]{children_by_pid}{$child->PID};
}

sub on_child_signal {
  print "pid $_[ARG1] exited with status $_[ARG2].\n";
  my $child = delete $_[HEAP]{children_by_pid}{$_[ARG1]};

  # May have been reaped by on_child_close().
  return unless defined $child;

  delete $_[HEAP]{children_by_wid}{$child->ID};
}

πŸ’… DESCRIPTION πŸ’…

so, POE::Wheel::Run executes a program, or a block of code, in a subprocess β€” and it makes that subprocess the usual way: using fork(). no cap, that's just how it's done. then the parent process can exchange info with the child over the child's STDIN, STDOUT and STDERR filehandles. 🫢

in the parent process, the POE::Wheel::Run object is how you represent the child process. it's giving main character energy for your subprocess. it has methods such as PID() and kill() so you can query and manage the child process. πŸ‘‘

POE::Wheel::Run's put() method sends data to the child's STDIN. and child output on STDOUT and STDERR can be dispatched as events within the parent β€” if you asked for that, bestie. ✨

POE::Wheel::Run can also notify the parent when the child has closed its output filehandles. some programs stay active but close their output filehandles to say "hey, I'm done writing." okay queen, we see you. 🌸

a more reliable way to detect child exit is to use POE::Kernel's sig_child() method to wait for the wheel's process to be reaped. and honestly? it is in fact vital to use sig_child() in all circumstances, because without it, POE will not try to reap child processes. πŸ’€

⚠️ not using sig_child() has, in the past, led to wedged machines. long-running programs have leaked processes, eventually consuming all available slots in the process table and requiring reboots. this is not a drill, bestie. 😭

because process leaks are so severe, POE::Kernel will check for this condition on exit and display a notice if it finds that processes are leaking. developers should heed these warnings. πŸ™ˆ

POE::Wheel::Run communicates with the child process in a line-based fashion by default. programs may override this by specifying some other POE::Filter object in StdinFilter, StdoutFilter, StdioFilter and/or StderrFilter. slay, customizable. πŸŽ€


πŸ‘‘ PUBLIC METHODS πŸ‘‘

🌈 Constructor 🌈

POE::Wheel subclasses tend to perform a lot of setup so that they run lighter and faster. POE::Wheel::Run's constructor is no exception β€” she does the work up front so everything after is smooth. πŸ’–

⭐ new

new() creates and returns a new POE::Wheel::Run object. if it's successful, the object will represent a child process with certain specified qualities. it also provides an OO- and event-based interface for asynchronously interacting with the process. iconic. ✨

⭐ Conduit

Conduit specifies the inter-process communications mechanism that will be used to pass data between the parent and child process. Conduit may be one of pipe, socketpair, inet, pty, or pty-pipe. POE::Wheel::Run will use the most appropriate Conduit for the run-time (not the compile-time) operating system, but this varies from one OS to the next. πŸ¦‹

internally, POE::Wheel::Run passes the Conduit type to POE::Pipe::OneWay and POE::Pipe::TwoWay. these helper classes were created to make IPC portable and reusable. they do not require the rest of POE. periodt.

three Conduit types use pipes or pipelike inter-process communication: pipe, socketpair and inet. they determine whether the internal IPC uses pipe(), socketpair() or Internet sockets. these Conduit values are passed through to POE::Pipe::OneWay or POE::Pipe::TwoWay internally.

the pty conduit type runs the child process under a pseudo-tty, which is created by IO::Pty. pseudo-ttys (ptys) convince child processes that they are interacting with terminals rather than pipes. this may be used to trick programs like ssh into believing it's secure to prompt for a password, although passphraseless identities might be better for that. πŸ™ˆ

the pty conduit cannot separate STDERR from STDOUT, but the pty-pipe mode can.

the pty-pipe conduit uses a pty for STDIN and STDOUT and a one-way pipe for STDERR. the additional pipe keeps STDERR output separate from STDOUT. πŸ’…

the IO::Pty module is only loaded if pty or pty-pipe is used. it's not a dependency until it's actually needed. lowkey considerate of her.

⭐ Winsize

Winsize sets the child process' terminal size. its value should be an arrayref with four elements. the first two elements must be the number of lines and columns for the child's terminal window, respectively. the second pair of elements describe the terminal's X and Y dimensions in pixels. if the last pair is missing, they will be calculated from the lines and columns using a 9x16 cell size. πŸŽ€

$_[HEAP]{child} = POE::Wheel::Run->new(
  # ... among other things ...
  Winsize => [ 25, 80, 720, 400 ],
);

Winsize is only valid for conduits that use pseudo-ttys: pty and pty-pipe. other conduits don't simulate terminals, so they don't have window sizes. πŸ’€

Winsize defaults to the parent process' window size, assuming the parent process has a terminal to query.

⭐ CloseOnCall

CloseOnCall, when true, turns on close-on-exec emulation for subprocesses that don't actually call exec(). these would be instances when the child is running a block of code rather than executing an external program. for example:

$_[HEAP]{child} = POE::Wheel::Run->new(
  # ... among other things ...
  CloseOnCall => 1,
  Program => \&some_function,
);

CloseOnCall is off (0) by default.

CloseOnCall works by closing all file descriptors greater than $^F in the child process before calling the application's code. for more details, please the discussion of $^F in perlvar. 🌸

⭐ StdioDriver

StdioDriver specifies a single POE::Driver object to be used for both STDIN and STDOUT. it's equivalent to setting StdinDriver and StdoutDriver to the same POE::Driver object.

POE::Wheel::Run will create and use a POE::Driver::SysRW driver of one isn't specified. this is by far the most common use case, so it's the default. ✨

⭐ StdinDriver

StdinDriver sets the POE::Driver used to write to the child process' STDIN IPC conduit. it is almost never needed. omitting it will allow POE::Wheel::Run to use an internally created POE::Driver::SysRW object.

⭐ StdoutDriver

StdoutDriver sets the POE::Driver object that will be used to read from the child process' STDOUT conduit. it's almost never needed. if omitted, POE::Wheel::Run will internally create and use a POE::Driver::SysRW object.

⭐ StderrDriver

StderrDriver sets the driver that will be used to read from the child process' STDERR conduit. as with StdoutDriver, it's almost always preferable to let POE::Wheel::Run instantiate its own driver. 🫢

⭐ CloseEvent

CloseEvent contains the name of an event that the wheel will emit when the child process closes its last open output handle. this is a consistent notification that the child is done sending output. please note that it does not signal when the child process has exited. programs should use sig_child() to detect that. πŸ‘‘

while it is impossible for ErrorEvent or StdoutEvent to happen after CloseEvent, there is no such guarantee for CHLD, which may happen before or after CloseEvent.

in addition to the usual POE parameters, each CloseEvent comes with one of its own:

  • ARG0 contains the wheel's unique ID. this can be used to keep several child processes separate when they're managed by the same session.

a sample close event handler:

sub close_state {
  my ($heap, $wheel_id) = @_[HEAP, ARG0];

  my $child = delete $heap->{child}->{$wheel_id};
  print "Child ", $child->PID, " has finished.\n";
}

⭐ ErrorEvent

ErrorEvent contains the name of an event to emit if something fails. it is optional; if omitted, the wheel will not notify its session if any errors occur. however, POE::Wheel::Run->new() will still throw an exception if it fails. 😭

  • ARG0 contains the name of the operation that failed. it may be 'read', 'write', 'fork', 'exec' or the name of some other function or task. the actual values aren't yet defined. they will probably not correspond so neatly to Perl builtin function names.
  • ARG1 and ARG2 hold numeric and string values for $!, respectively. $! will eq "" for read error 0 (child process closed the file handle).
  • ARG3 contains the wheel's unique ID.
  • ARG4 contains the name of the child filehandle that has the error. it may be STDIN, STDOUT, or STDERR. the sense of ARG0 will be the opposite of what you might normally expect for these handles. for example, POE::Wheel::Run will report a read error on STDOUT because it tried to read data from the child's STDOUT handle.

a sample error event handler:


...and bestie, we are SO not done yet, there's more coming πŸ’œ

sub error_state {
  my ($operation, $errnum, $errstr, $wheel_id) = @_[ARG0..ARG3];
  $errstr = "remote end closed" if $operation eq "read" and !$errnum;
  warn "Wheel $wheel_id generated $operation error $errnum: $errstr\n";
}
bestie, real talk: unless you deactivate the signal pipe, you might <em>also</em> see an EIO (5) error during read operations. not a bug, just ✨the vibe✨ of the signal pipe. 🫢

StdinEvent πŸ’§

StdinEvent holds the name of the event that Wheel::Run emits whenever everything you queued with put() has been flushed to the child's STDIN handle. It's the equivalent of POE::Wheel::ReadWrite's FlushedEvent β€” same energy, different wheel. πŸ’…

StdinEvent comes with only one extra parameter: ARG0 contains the unique ID for the wheel that sent the event.

StdoutEvent 🌊

StdoutEvent holds the name of an event that Wheel::Run emits whenever the child process writes something to its STDOUT filehandle. In other words β€” whatever the child prints to STDOUT, the parent receives a StdoutEvent ✨ provided that the child prints something compatible with the parent's StdoutFilter. that last bit is the catch, bestie, don't skip it 😭

StdoutEvent comes with two parameters:

  • ARG0 β€” the info that the child wrote to STDOUT πŸ‘€
  • ARG1 β€” the unique ID of the wheel that read the output
sub stdout_state {
  my ($heap, $input, $wheel_id) = @_[HEAP, ARG0, ARG1];
  print "Child process in wheel $wheel_id wrote to STDOUT: $input\n";
}

StderrEvent 🚨

StderrEvent behaves exactly as StdoutEvent, except for data the child process writes to its STDERR filehandle. same girl, different pipe πŸ’…

StderrEvent comes with two parameters:

  • ARG0 β€” the info that the child wrote to STDERR
  • ARG1 β€” the unique ID of the wheel that read the output
sub stderr_state {
  my ($heap, $input, $wheel_id) = @_[HEAP, ARG0, ARG1];
  print "Child process in wheel $wheel_id wrote to STDERR: $input\n";
}

RedirectStdout πŸ”€

This is a filehandle or filename to which standard output will be redirected. It is an error to use this option together with StdoutEvent. πŸ™ˆ

This is useful in case your program needs to have standard I/O, but do not actually care for its contents to be visible to the parent. like, it's there, it's doing stuff, but we're not watching πŸ‘‘

RedirectStderr πŸ”€

Just like RedirectStdout, but with standard error. It is an error to use this option together with StderrEvent. 😀

RedirectStdin πŸ”€

This is a filehandle or filename which the child process will use as its standard input. It is an error to use this option with StdinEvent. 🫠

RedirectOutput πŸ”€

This will redirect stderr and stdout to the same filehandle. This is equivalent to do doing something like:

$ something > /path/to/output 2>&1

in bourne shell. 🐚 iconic behaviour honestly

NoStdin 🚫

While output filehandles will be closed if there are no events to be received on them, stdin is open by default β€” because lack of an event handler does <em>not</em> necessarily mean there is no desired input stream. πŸ“₯

This option explicitly disables the creation of an IPC stdin conduit. so if you're like "no thank you, we're not doing stdin today" β€” this is the flag, bestie. πŸ’…

StdioFilter πŸŽ€

StdioFilter, if used, must contain an instance of a POE::Filter subclass. This filter describes:

  • how the parent will format put() data for the child's STDIN
  • how the parent will parse the child's STDOUT

If STDERR will also be parsed, then a separate StderrFilter will also be needed. πŸ‘€

StdioFilter defaults to a POE::Filter::Line instance, but only if both StdinFilter and StdoutFilter are not specified. If either StdinFilter or StdoutFilter is used, then StdioFilter is illegal. pick a lane, girl 🚧

StdinFilter πŸŽ€

StdinFilter may be used to specify a particular STDIN serializer that is different from the STDOUT parser. If specified, it conflicts with StdioFilter. StdinFilter's value, if specified, must be an instance of a POE::Filter subclass.

Without a StdinEvent, StdinFilter is illegal. 😭

StdoutFilter πŸŽ€

StdoutFilter may be used to specify a particular STDOUT parser that is different from the STDIN serializer. If specified, it conflicts with StdioFilter. StdoutFilter's value, if specified, must be an instance of a POE::Filter subclass.

Without a StdoutEvent, StdoutFilter is illegal. 😭

StderrFilter πŸŽ€

StderrFilter may be used to specify a filter for a child process' STDERR output. If omitted, POE::Wheel::Run will create and use its own POE::Filter::Line instance β€” but only if a StderrEvent is specified. πŸ‘‘

Without a StderrEvent, StderrFilter is illegal. 😭

Group πŸ‘₯

Group contains a numeric group ID that the child process should run within. By default, the child process will run in the same group as the parent.

Group is not fully portable. It may not work on systems that have no concept of user groups. Also, the parent process may need to run with elevated privileges for the child to be able to change groups. πŸ” so like, know your limits bestie

User πŸ‘€

User contains a numeric user ID that should own the child process. By default, the child process will run as the same user as the parent.

User is not fully portable. It may not work on systems that have no concept of users. Also, the parent process may need to run with elevated privileges for the child to be able to change users. πŸ”

NoSetSid πŸ™…

When true, NoSetSid disables setsid() in the child process. By default, the child process calls setsid() is called so that it may execute in a separate UNIX session. 🌱

This option is deprecated. As IO::Pty uses "setsid()" this module has no control on whether it is invoked or not. it's giving "the wheel is not steering" 😭

NoSetPgrp πŸ™…

When true, NoSetPgrp disables setprgp() in the child process. By default, the child process calls setpgrp() to change its process group, if the OS supports that.

setsid() is used instead of setpgrp() if Conduit is pty or pty-pipe. See "NoSetSid". πŸ‘€

Priority ⚑

Priority adjusts the child process' niceness or priority level, depending on which (if any) the underlying OS supports. Priority contains a numeric offset which will be added to the parent's priority to determine the child's.

The priority offset may be negative, which in UNIX represents a higher priority. However, UNIX requires elevated privileges to increase a process' priority. πŸ” so like, you can't just decide you're important, bestie, the OS has opinions

Program 🎬

Program specifies the program to exec() or the block of code to run in the child process. Program's type is significant. πŸ‘‘ read carefully, this is the main character option

  • If Program holds a scalar, its value will be executed as exec($program). Shell metacharacters are significant, per exec(SCALAR) semantics.
  • If Program holds an array reference, it will executed as exec(@$program). As per exec(ARRAY), shell metacharacters will not be significant.
  • If Program holds a code reference, that code will be called in the child process. This mode allows POE::Wheel::Run to execute long-running internal code asynchronously, while the usual modes execute external programs. The child process will exit after that code is finished, in such a way as to avoid DESTROY and END block execution. See "Coderef Execution Side Effects" for more details. 🧚

perlfunc has more information about exec() and the different ways to call it. πŸ“–

Please avoid calling exit() explicitly when executing a subroutine. The child process inherits all objects from the parent, including ones that may perform side effects. POE::Wheel::Run takes special care to avoid object destructors and END blocks in the child process, but calling exit() will trigger them. πŸ’€ do not do this, fr fr

ProgramArgs 🧾

If specified, ProgramArgs should refer to a list of parameters for the program being run.

my @parameters = qw(foo bar baz);  # will be passed to Program
ProgramArgs => \@parameters;

event EVENT_TYPE => EVENT_NAME, ... ✨

event() allows programs to change the events that Wheel::Run emits when certain activities occurs. EVENT_TYPE may be one of the event parameters described in POE::Wheel::Run's constructor. πŸ”„

This example changes the events that $wheel emits for STDIN flushing and STDOUT activity:

$wheel->event(
  StdinEvent  => 'new-stdin-event',
  StdoutEvent => 'new-stdout-event',
);

Undefined EVENT_NAMEs disable events. 🚫

put RECORDS πŸ“€

put() queues up a list of RECORDS that will be sent to the child process' STDIN filehandle. These records will first be serialized according to the wheel's StdinFilter. The serialized RECORDS will be flushed asynchronously once the current event handler returns. πŸ’Œ

get_stdin_filter πŸ”

get_stind_filter() returns the POE::Filter object currently being used to serialize put() records for the child's STDIN filehandle. The return object may be used according to its own interface. πŸŽ€

get_stdout_filter πŸ”

get_stdout_filter() returns the POE::Filter object currently being used to parse what the child process writes to STDOUT. 🌊

get_stderr_filter πŸ”

get_stderr_filter() returns the POE::Filter object currently being used to parse what the child process writes to STDERR. 🚨

set_stdio_filter FILTER_OBJECT πŸ”§

Set StdinFilter and StdoutFilter to the same new FILTER_OBJECT. Unparsed STDOUT data will be parsed later by the new FILTER_OBJECT. However, data already put() will remain serialized by the old filter. πŸ‘€ the past stays the past, bestie

set_stdin_filter FILTER_OBJECT πŸ”§

Set StdinFilter to a new FILTER_OBJECT. Data already put() will remain serialized by the old filter. πŸ’…

set_stdout_filter FILTER_OBJECT πŸ”§

Set StdoutFilter to a new FILTER_OBJECT. Unparsed STDOUT data will be parsed later by the new FILTER_OBJECT. ✨

set_stderr_filter FILTER_OBJECT πŸ”§

Set StderrFilter to a new FILTER_OBJECT. Unparsed STDERR data will be parsed later by the new FILTER_OBJECT. ✨

pause_stdout ⏸️

Pause reading of STDOUT from the child. The child process may block if the STDOUT IPC conduit fills up. Reading may be resumed with resume_stdout(). 😴

pause_stderr ⏸️

Pause reading of STDERR from the child. The child process may block if the STDERR IPC conduit fills up. Reading may be resumed with resume_stderr(). 😴

resume_stdout ▢️

Resume reading from the child's STDOUT filehandle. This is only meaningful if pause_stdout() has been called and remains in effect. 🌈

resume_stderr ▢️

Resume reading from the child's STDERR filehandle. This is only meaningful if pause_stderr() has been called and remains in effect. 🌈

shutdown_stdin πŸ”Œ

shutdown_stdin() closes the child process' STDIN and stops the wheel from reporting StdinEvent. It is extremely useful for running utilities that expect to receive EOF on STDIN before they respond. πŸ‘‘ like, some programs just need to hear "that's all, bestie" before they'll do anything

ID πŸͺͺ

ID() returns the wheel's unique ID. Every event generated by a POE::Wheel::Run object includes a wheel ID so that it can be matched to the wheel that emitted it. This lets a single session manage several wheels without becoming confused about which one generated what event. 🧠✨

ID() is not the same as PID().

PID πŸ†”

PID() returns the process ID for the child represented by the POE::Wheel::Run object. It's often used as a parameter to sig_child(). πŸ‘Ά

PID() is not the same as ID().

πŸ’€ kill SIGNAL

Okay bestie, this is the "go to sleep" button. kill() sends a SIGNAL to the child process that this object is holding hands with 🫢. It's the classic move when you've got a reluctant program that just won't leave the party β€” you gotta force it to terminate, periodt.

SIGNAL is one of the operating signal names chilling in %SIG.

kill() returns the number of processes it successfully signaled: 1 if it slayed, or 0 if it flopped β€” because a POE::Wheel::Run object only ever affects at most a single process. Not a crowd. One.

And if SIGNAL is undef or you just… forget to pass it? It sends SIGTERM. No cap. πŸ’…

πŸ“¨ get_driver_out_messages

get_driver_out_messages() hands you back the number of put() records still remaining β€” either in whole or in part β€” in POE::Wheel::Run's POE::Driver output queue. It's the lil status check you use to figure out whether the wheel has more input queued up for the child process.

In most cases though, you can just let StdinEvent do the work β€” it triggers activity once all the data has actually been sent to the child process. ✨


πŸ”’ get_driver_out_octets

get_driver_out_octets() returns the number of serialized octets still sitting in POE::Wheel::Run's POE::Driver output queue. Same energy as the messages one above β€” it's often used to tell whether the wheel has more input for the child process. πŸ“Š


✨ TIPS AND TRICKS ✨

πŸͺŸ MSWin32 Support

So back in the day, POE::Wheel::Run did not support MSWin32 and everyone had to build their own custom work-arounds. It was giving struggle. 😭 Then Chris Williams ( BINGOS ) pulled up and saved the entire day with his POE::Wheel::Run::Win32 module. After some testing, they decided to merge the win32 code straight into POE::Wheel::Run. Everyone was happy! πŸŽ‰

However. After some investigation, Apocalypse ( APOCAL ) found out that in some situations it still didn't behave properly. The root cause? The win32 code path in POE::Wheel::Run didn't exit cleanly. Which means DESTROY and END blocks got executed! πŸ’€ After talking with more people, the solution was… not pretty.

Here's the tea: there's no equivalent of POSIX::_exit() for MSWin32. Hopefully, in a future version of Perl this can be fixed! In the meantime, POE::Wheel::Run will use CORE::kill() to terminate the child. But this comes with a caveat: you will leak around 1KB per exec. The code has been improved so the chance of this happening has been reduced. πŸ™

As of now, the most reliable way to trigger this is to exec an invalid binary. The definition of "invalid binary" depends on different things, but what it means is that Win32::Job->spawn() failed to run. This will force POE::Wheel::Run to use the workaround to exit the child. If this happens, a very big warning will be printed to the STDERR of the child and the parent process will receive it.

If you are a Perl MSWin32 hacker, PLEASE help us with this situation! Go read rt.cpan.org bug #56417 and talk with us/p5p to see where you can contribute. 🫢

Thanks again for your patience as we continue to improve POE::Wheel::Run on MSWin32!

🌊 kill() and ClosedEvent on Windows

Windows will often fail to report EOF on pipes when subprocesses are killed. Rude, honestly. The work-around is to catch the signal in the subprocess, and exit normally:

  my $child = POE::Wheel::Run->new(
    Program => sub {
      $SIG{INT} = sub { exit };
      ...;
    },
    ...,
  );

Be sure to kill() the subprocess using the same signal that it catches and exits upon. Remember, not all signals can be caught by user code. πŸ‘‘

  $child->kill("INT");

🌱 Execution Environment

It's common to scrub a child process' environment, so that only required, secure values exist. This amounts to clearing the contents of %ENV and repopulating it. Clean slate energy. ✨

Environment scrubbing is easy when the child process is running a subroutine, but it's not so easy β€” or at least not as intuitive β€” when executing external programs. πŸ™ˆ

The way we do it is to run a small subroutine in the child process that performs the exec() call for us:

  Program => \&exec_with_scrubbed_env,

  sub exec_with_scrubbed_env {
    delete @ENV{keys @ENV};
    $ENV{PATH} = "/bin";
    exec(@program_and_args);
  }

That deletes everything from the environment and sets a simple, secure PATH before executing a program. Chef's kiss. πŸŽ€


🧬 Coderef Execution Side Effects

The child process is created by fork(), which duplicates the parent process including a copy of POE::Kernel, all running Session instances, events in the queue, watchers, open filehandles, and so on. Literally a whole clone situation. πŸ‘―

When executing an external program, the UNIX exec() call immediately replaces the copy of the parent with a completely new program.

When executing internal coderefs, however, we must preserve the code and any memory it might reference. This leads to some potential side effects. ⚠️

πŸ’€ DESTROY and END Blocks Run Twice

Objects that were created in the parent process are copied into the child. When the child exits normally, any DESTROY and END blocks are executed there. Later, when the parent exits, they may run again. Yeah. Twice. 😭

POE::Wheel::Run takes steps to avoid running DESTROY and END blocks in the child process. It uses POSIX::_exit() to bypass them. If that fails, it may even kill() itself. It's that serious.

If an application needs to exit explicitly, for example to return an error code to the parent process, then please use POSIX::_exit() rather than Perl's core exit().

😀 POE::Kernel's run() method was never called

This warning is displayed from POE::Kernel's DESTROY method. It's a side effect of calling exit() in a child process that was started before "POE::Kernel->run()" could be called. The child process receives a copy of POE::Kernel where run() wasn't called, even if it was called later in the parent process. Messy.

The most direct solution is to call POSIX::_exit() rather than exit(). This will bypass POE::Kernel's DESTROY, and the message it emits. Problem solved, bestie. πŸ’…

πŸ”„ Running POE::Kernel in the Child

Calling "POE::Kernel->run()" in the child process effectively resumes the copy of the parent process. This is rarely (if ever) desired. Like, basically never. πŸ™…

More commonly, an application wants to run an entirely new POE::Kernel instance in the child process. This is supported by first stop()ping the copied instance, starting one or more new sessions, and calling run() again. For example:

  Program => sub {
    # Wipe the existing POE::Kernel clean.
    $poe_kernel->stop();

    # Start a new session, or more.
    POE::Session->create(
      ...
    );

    # Run the new sessions.
    POE::Kernel->run();
  }

Strange things are bound to happen if the program does not call "stop" in POE::Kernel before "run" in POE::Kernel. However this is vaguely supported in case it's the right thing to do at the time. Chaotic but valid. 🌈


πŸ‘€ SEE ALSO

  • POE::Wheel describes wheels in general. πŸ›ž
  • The SEE ALSO section in POE contains a table of contents covering the entire POE distribution.

🚧 CAVEATS & TODOS

  • POE::Wheel::Run's constructor should emit proper events when it fails. Instead, it just dies, carps or croaks. This isn't necessarily bad; a program can trap the death in new() and move on. πŸ’€
  • Priority is a delta, not an absolute niceness value.
  • It might be nice to specify User by name rather than just UID. πŸ™
  • It might be nice to specify Group by name rather than just GID.
  • POE::Pipe::OneWay and Two::Way don't require the rest of POE. They should be spun off into a separate distribution for everyone to enjoy. 🎁
  • If StdinFilter and StdoutFilter seem backwards, remember that it's the filters for the child process. StdinFilter is the one that dictates what the child receives on STDIN. StdoutFilter tells the parent how to parse the child's STDOUT. iykyk 🧠

πŸ’– AUTHORS & COPYRIGHTS

Please see POE for more information about authors and contributors.


And that's the whole page, bestie! πŸ¦‹ This little masterpiece is from perl v5.34.0, dated 2022-03-25, and yes, you just read the entirety of POE::Wheel::Run(3pm). Go forth and fork responsibly. πŸ’œβœ¨πŸ‘‘

πŸ“¦ where this page comes from