#!/usr/bin/env perl
# vim: fdm=marker sw=4 et
package IO::Pty::HalfDuplex::Ptyish;
# Notes on design {{{
# IO::Pty::HalfDuplex operates by mimicing a job-control shell. A process
# is done sending data when it calls read, which we notice because it
# results in Stopped (tty input). So far, fairly simple. Complications
# arise because of races, and also because shells are required to run in
# the managed tty, and be the parent of the process; this forces us to use
# a stub process and simple IPC.
# }}}
# POD header {{{
=head1 NAME
IO::Pty::HalfDuplex::Ptyish - Base class for pty-using HalfDuplex backends
=head1 SYNOPSIS
package IO::Pty::HalfDuplex::PTrace;
use base 'IO::Pty::HalfDuplex::Ptyish';
sub shell {
my %args = @_;
#start subprocess
syswrite $args->{info_pipe}, pack("N", $pid);
while(1) {
# wait for subprocess to block
if (subprocess died) {
syswrite $args->{info_pipe}, "d" . pack("CC", $sig, $code);
POSIX::_exit();
}
syswrite $args->{info_pipe}, "r";
sysread $args->{ctl_pipe}, $_, 1;
# continue subprocess
}
}
1;
=head1 DESCRIPTION
C<IO::Pty::HalfDuplex::Ptyish> is the base class for pty-using HalfDuplex
backends. It implements the HalfDuplex methods by opening a pty and starting
a slave process to control the child; this slave communicates with the main
process using a pair of pipes. Subclasses must implement the C<shell()>
method, with the following specification:
=head2 $pty->shell(info_pipe => $status_fh, ctl_pipe => $control_fh,
command => \@argv)
C<shell> forks and starts the child process as if by C<exec(@argv)>. It then
writes the PID of the child in C<pack "N"> format to $status_fh, and enters
an infinite loop in the parent. Each time the child stops waiting for input,
the character "r" is written to $status_fd; the client process will request
a restart by putting more data into the pty buffer and writing "s" to
$control_fh. When the child exits, write a "d" to $status_fd, followed
by the child's exit signal or 0 and exit code or 0, each in C<pack "C"> format.
The shell then calls _exit.
=cut
# }}}
# Imports {{{
use strict;
use warnings;
use base 'IO::Pty::HalfDuplex';
use POSIX qw(:unistd_h :sys_wait_h :signal_h EIO);
use Carp;
use IO::Pty;
use Time::HiRes qw(time);
our $_infinity = 1e1000;
# }}}
# new {{{
# Most of this is handled by IO::Pty, thankfully
sub new {
my $class = shift;
my $self = {
# options
buffer_size => 8192,
@_,
# state
pty => undef,
active => 0,
exit_code => undef,
};
bless $self, $class;
$self->{pty} = new IO::Pty;
return $self;
}
# }}}
sub spawn {
my $self = shift;
my $slave = $self->{pty}->slave;
croak "Attempt to spawn a subprocess when one is already running"
if $self->is_active;
pipe (my $p1r, my $p1w) || croak "Failed to create a pipe";
pipe (my $p2r, my $p2w) || croak "Failed to create a pipe";
$self->{info_pipe} = $p1r;
$self->{ctl_pipe} = $p2w;
defined ($self->{shell_pid} = fork) || croak "fork: $!";
unless ($self->{shell_pid}) {
close $p1r;
close $p2w;
$self->{pty}->make_slave_controlling_terminal;
close $self->{pty};
$slave->set_raw;
# reopen the standard file descriptors in the child to point to the
# pty rather than wherever they have been pointing during the script's
# execution
open(STDIN, "<&" . $slave->fileno)
or carp "Couldn't reopen STDIN for reading";
open(STDOUT, ">&" . $slave->fileno)
or carp "Couldn't reopen STDOUT for writing";
open(STDERR, ">&" . $slave->fileno)
or carp "Couldn't reopen STDERR for writing";
close $slave;
$self->_shell(info_pipe => $p1w, ctl_pipe => $p2r,
command => [@_]);
}
close $p1w;
close $p2r;
$self->{pty}->close_slave;
$self->{pty}->set_raw;
my ($rcpid);
my $syncd = sysread($self->{info_pipe}, $rcpid, 4);
unless ($syncd == 4) {
croak "Cannot sync with child: $!";
}
$self->{slave_pgid} = unpack "N", $rcpid;
$self->{read_buffer} = $self->{write_buffer} = '';
$self->{sent_sync} = 0; $self->{active} = 1;
$self->{timeout} = $self->{exit_code} = $self->{exit_sig} = undef;
}
# }}}
# I/O on shell pipes {{{
# Process a wait result from the shell
sub _handle_info_read {
my $self = shift;
my $ibuf;
my $ret = sysread $self->{info_pipe}, $ibuf, 1;
if ($ret == 0) {
# Shell has exited
$self->{sent_sync} = 0;
$self->{active} = 0;
# FreeBSD 7 (and presumably other BSDkin) requires the pty output
# buffer to be drained before any session leader can exit.
$self->_handle_pty_drain;
# Reap the shell
waitpid($self->{shell_pid}, 0);
if (!defined $self->{exit_code}) {
# Get the shell crash code
$self->{exit_sig} = WIFSIGNALED($?) ? WTERMSIG($?) : 0;
$self->{exit_code} = WIFEXITED($?) ? WEXITSTATUS($?) : 0;
}
} elsif ($ibuf eq 'd') {
sysread $self->{info_pipe}, $ibuf, 2;
@{$self}{"exit_sig","exit_code"} = unpack "CC", $ibuf;
} elsif ($ibuf eq 'r') {
$self->{sent_sync} = 0;
}
}
sub _handle_pty_write {
my ($self, $ref) = @_;
my $ct = syswrite $self->{pty}, $self->{write_buffer}
or die "write(pty): $!";
$self->{write_buffer} = substr($self->{write_buffer}, $ct);
}
sub _handle_pty_read {
my ($self) = @_;
return if defined (sysread $self->{pty}, $self->{read_buffer},
$self->{buffer_size}, length $self->{read_buffer});
# Under Linux, any pty read can randomly return EIO if the
# session leader exits racily.
return if $! == &POSIX::EIO and $^O eq "linux";
die "read(pty): $!";
}
sub _handle_pty_drain {
my ($self) = @_;
while (1) {
my $got = sysread $self->{pty}, $self->{read_buffer},
$self->{buffer_size}, length $self->{read_buffer};
return if defined $got && $got == 0;
next if defined $got;
# Under Linux, any pty read can randomly return EIO if the
# session leader exits racily.
return if $! == &POSIX::EIO and $^O eq "linux";
die "drain(pty): $!";
}
}
# }}}
# Read internals {{{
# A little something to make all these select loops nicer
sub _select_loop {
my ($self, $block, $pred) = splice @_, 0, 3;
while ($pred->()) {
my %mask = ('r' => '', 'w' => '', 'x' => '');
my $tmo = !$block ? 0 :
defined $self->{timeout} ? $self->{timeout} - time : undef;
for (@_) {
vec($mask{$_->[1]}, fileno($_->[0]), 1) = 1
if @$_ < 4 || $_->[3];
}
return 1 if ($tmo||0)< 0 || !select($mask{r}, $mask{w}, $mask{x}, $tmo);
for (@_) {
$_->[2]() if vec($mask{$_->[1]}, fileno($_->[0]), 1);
}
}
}
# We want to return when the slave has processed all input. We have to
# break it up into pty-buffer-sized chunks, though.
sub _process_wait {
my ($self) = shift;
$self->_select_loop(1 => sub{ $self->{sent_sync} },
[ $self->{info_pipe}, r => sub { $self->_handle_info_read() } ],
[ $self->{pty}, r => sub { $self->_handle_pty_read() } ]);
}
# Send as much data as possible
sub _process_send {
my ($self) = @_;
$self->_select_loop(0 => sub{ $self->{write_buffer} ne '' },
[ $self->{info_pipe}, r => sub { $self->_handle_info_read() } ],
[ $self->{pty}, r => sub { $self->_handle_pty_read() } ],
[ $self->{pty}, w => sub { $self->_handle_pty_write() } ]);
}
sub _send_sync {
my $self = shift;
return if $self->{sent_sync};
syswrite $self->{ctl_pipe}, "s";
$self->{sent_sync} = 1;
}
# }}}
# I/O operations {{{
sub recv {
my ($self, $timeout) = @_;
if (! $self->is_active) {
carp "Reading from dead slave";
return;
}
$self->{timeout} = defined $timeout ? $timeout + time : undef;
do {
$self->_process_send();
$self->_send_sync();
return undef if $self->_process_wait();
} while ($self->{write_buffer} ne '' && $self->{active});
my $t = $self->{read_buffer};
$self->{read_buffer} = '';
$t;
}
sub write {
my ($self, $text) = @_;
if (! $self->is_active) {
carp "Writing to dead slave";
return;
}
$self->{write_buffer} .= $text;
}
sub is_active {
my $self = shift;
return $self->{active};
}
sub _wait_for_inactive {
my $self = shift;
my $targ = shift;
$targ = defined $targ ? $targ + time : undef;
do {
$self->recv(defined $targ ? $targ - time : undef);
} while ($targ > time && $self->is_active);
!$self->is_active;
}
# }}}
# kill() {{{
sub kill {
my $self = shift;
if (@_ < 2) { @_ = (TERM => 3, KILL => 3); }
return 1 if !$self->is_active;
while (@_ >= 2) {
my ($sig, $tme) = splice @_, 0, 2;
kill $sig => -$self->{slave_pgid}
or return undef;
$tme = defined $tme ? $tme : $_infinity;
if ($tme && $self->_wait_for_inactive($tme)) {
return 1;
}
}
return 0;
}
# }}}
# close() {{{
sub close {
my $self = shift;
$self->kill;
close $self->{pty};
$self->{pty} = undef;
}
# }}}
# documentation tail {{{
sub _shell {
my $class = ref(shift);
die ($class eq 'IO::Pty::HalfDuplex::Ptyish')
? "You must subclass Ptyish, not use it directly"
: "You need to override shell() in Ptyish subclasses";
}
1;
__END__
=head1 AUTHOR
Stefan O'Rear, C<< <stefanor@cox.net> >>
=head1 BUGS
No known bugs.
Please report any bugs through RT: email
C<bug-io-halfduplex at rt.cpan.org>, or browse
L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=IO-HalfDuplex>.
=head1 COPYRIGHT AND LICENSE
Copyright 2008-2009 Stefan O'Rear.
This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.
=cut
# }}}