mirror of https://github.com/pkolano/sshim.git
636 lines
22 KiB
Perl
Executable File
636 lines
22 KiB
Perl
Executable File
#!/usr/bin/perl
|
|
#
|
|
# Notices
|
|
# Copyright (C) 2019 United States Government as represented by the
|
|
# Administrator of the National Aeronautics and Space Administration.
|
|
# All Rights Reserved.
|
|
#
|
|
# Disclaimers
|
|
#
|
|
# No Warranty: THE SUBJECT SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY
|
|
# WARRANTY OF ANY KIND, EITHER EXPRESSED, IMPLIED, OR STATUTORY,
|
|
# INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY THAT THE SUBJECT SOFTWARE
|
|
# WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
|
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR FREEDOM FROM
|
|
# INFRINGEMENT, ANY WARRANTY THAT THE SUBJECT SOFTWARE WILL BE ERROR FREE,
|
|
# OR ANY WARRANTY THAT DOCUMENTATION, IF PROVIDED, WILL CONFORM TO THE
|
|
# SUBJECT SOFTWARE. THIS AGREEMENT DOES NOT, IN ANY MANNER, CONSTITUTE AN
|
|
# ENDORSEMENT BY GOVERNMENT AGENCY OR ANY PRIOR RECIPIENT OF ANY RESULTS,
|
|
# RESULTING DESIGNS, HARDWARE, SOFTWARE PRODUCTS OR ANY OTHER APPLICATIONS
|
|
# RESULTING FROM USE OF THE SUBJECT SOFTWARE. FURTHER, GOVERNMENT AGENCY
|
|
# DISCLAIMS ALL WARRANTIES AND LIABILITIES REGARDING THIRD?PARTY SOFTWARE,
|
|
# IF PRESENT IN THE ORIGINAL SOFTWARE, AND DISTRIBUTES IT "AS IS."
|
|
#
|
|
# Waiver and Indemnity: RECIPIENT AGREES TO WAIVE ANY AND ALL CLAIMS
|
|
# AGAINST THE UNITED STATES GOVERNMENT, ITS CONTRACTORS AND
|
|
# SUBCONTRACTORS, AS WELL AS ANY PRIOR RECIPIENT. IF RECIPIENT'S USE OF
|
|
# THE SUBJECT SOFTWARE RESULTS IN ANY LIABILITIES, DEMANDS, DAMAGES,
|
|
# EXPENSES OR LOSSES ARISING FROM SUCH USE, INCLUDING ANY DAMAGES FROM
|
|
# PRODUCTS BASED ON, OR RESULTING FROM, RECIPIENT'S USE OF THE SUBJECT
|
|
# SOFTWARE, RECIPIENT SHALL INDEMNIFY AND HOLD HARMLESS THE UNITED STATES
|
|
# GOVERNMENT, ITS CONTRACTORS AND SUBCONTRACTORS, AS WELL AS ANY PRIOR
|
|
# RECIPIENT, TO THE EXTENT PERMITTED BY LAW. RECIPIENT'S SOLE REMEDY FOR
|
|
# ANY SUCH MATTER SHALL BE THE IMMEDIATE, UNILATERAL TERMINATION OF THIS
|
|
# AGREEMENT.
|
|
#
|
|
|
|
# This program provides transparent resilience for ssh remote commands
|
|
# provided it is installed on both the local and remote host and that
|
|
# ssh batch mode authentication is being used (e.g. publickey or
|
|
# hostbased). A SSHIM session consists of three processes (all
|
|
# implemented by this program):
|
|
# 1. a local process sitting between the original command and ssh
|
|
# 2. a remote process sitting between ssh and the remote SSHIM daemon
|
|
# 3. a remote daemon sitting between the remote process and the remote
|
|
# side of the original command
|
|
# There is only one instance of the local process and remote daemon,
|
|
# but the remote process may be spawned as many times as needed to
|
|
# overcome ssh connection interruptions.
|
|
#
|
|
# It is possible for remote daemons to be left running if the local
|
|
# process is not allowed to terminate cleanly (e.g. kill -9 used).
|
|
# It is also possible that the code experiences a failure scenario
|
|
# on which it was not tested (none currently known, but inevitable
|
|
# due to wide array of command/failure behavior - please report to
|
|
# pkolano@gmail.com if encountered).
|
|
|
|
use strict;
|
|
use File::Temp qw(tempdir);
|
|
use Getopt::Long qw(:config bundling no_ignore_case require_order);
|
|
use IO::Select;
|
|
use IO::Socket::UNIX;
|
|
use IPC::Open3;
|
|
use List::Util qw(max min);
|
|
use POSIX qw(setsid :sys_wait_h);
|
|
use Symbol qw(gensym);
|
|
|
|
our $VERSION = 1.0;
|
|
|
|
use constant EOFFOE => "SSHIM_EOF" . "FOE_MIHSS";
|
|
use constant EOFFOE_LEN => length(EOFFOE);
|
|
# this is the max size of pipe reads/writes
|
|
use constant IO_SIZE => 8192;
|
|
use constant BUF_SIZE => 1024 * IO_SIZE;
|
|
|
|
# 1 = local, 2 = remote, 3 = daemon
|
|
my $self;
|
|
|
|
my %opts = (
|
|
try => 0,
|
|
timeout => 10,
|
|
);
|
|
# these options (besides -h) are for internal use only
|
|
die "Invalid options\n" if (!GetOptions(\%opts,
|
|
"help|h", "remote", "try=i", "sockdir=s",
|
|
));
|
|
if ($opts{help} || !scalar(@ARGV)) {
|
|
print "Usage: sshim ssh/dropbear [OPTION]... HOST COMMAND [ARGUMENT]...\n";
|
|
print "\n";
|
|
print " - Provides transparent resilience for COMMAND on HOST\n";
|
|
print " - Requires sshim in default \$PATH on HOST\n";
|
|
print " - Requires batch mode authentication (e.g. publickey/hostbased)\n";
|
|
print "\n";
|
|
print "For use with scp/sftp, a simple wrapper must be created:\n";
|
|
print "\n";
|
|
print " echo -e '#!/bin/sh\\nexec sshim ssh' > ~/bin/swrap\n";
|
|
print " chmod 700 ~/bin/swrap\n";
|
|
print "\n";
|
|
print "Examples:\n";
|
|
print "\n";
|
|
print " - rsync -e 'sshim ssh' -r /src/dir host.example.com:/dst/dir\n";
|
|
print " - sshim ssh bastion.example.com ssh host.example.com cat /big/file\n";
|
|
print " - scp -S ~/bin/swrap -r host.example.com:/src/dir /dst/dir\n";
|
|
print " - sftp -S ~/bin/swrap -r host.example.com:/src/dir /dst/dir\n";
|
|
exit;
|
|
}
|
|
|
|
my $iargv;
|
|
if (!$opts{remote} && $ARGV[0] =~ /(^|\/)(ssh|dropbear)$/) {
|
|
###############
|
|
#### local ####
|
|
###############
|
|
$self = 1;
|
|
my @save_args = @ARGV;
|
|
while ($ARGV[0] =~ /(^|\/)(ssh|dropbear)$/) {
|
|
# shift off ssh
|
|
shift @ARGV;
|
|
# find location of final remote command by parsing ssh options
|
|
GetOptions(
|
|
qw(a f g k n q s t v x y),
|
|
qw(A C G K M N T V X Y),
|
|
qw(1 2 4 6),
|
|
qw(b=s c=s e=s i=s l=s m=s p=i w=s),
|
|
qw(D=s E=s F=s I=s J=s L=s O=s Q=s R=s S=s W=s),
|
|
"o=s" => sub {
|
|
# handle all -oKey=Val options of ssh
|
|
my ($key, $val) = split(/=|\s+/, $_[1]);
|
|
$val = shift @ARGV if (!defined $val);
|
|
$opts{$_[0] . lc($key)} = $val;
|
|
}
|
|
);
|
|
# shift off host name
|
|
shift @ARGV;
|
|
}
|
|
# insert sshim before final remote command
|
|
my @new = ("sshim", "--remote");
|
|
$iargv = -scalar(@ARGV);
|
|
splice(@save_args, $iargv, 0, @new);
|
|
@ARGV = @save_args;
|
|
}
|
|
|
|
my ($pid, $pipe, @child, @caller);
|
|
my @rbytes = (0, 0, 0);
|
|
my @bufs = ("", "", "");
|
|
my @ibufs = (0, 0, 0);
|
|
my @eof = (0, 0, 0);
|
|
|
|
$SIG{PIPE} = sub {
|
|
$pipe = 1;
|
|
};
|
|
|
|
my @sock;
|
|
my $sel = IO::Select->new;
|
|
if ($opts{remote} && !$opts{sockdir}) {
|
|
##############################
|
|
#### remote before daemon ####
|
|
##############################
|
|
# print sockdir for client side
|
|
$opts{sockdir} = tempdir("sshim-XXXXXXXX", TMPDIR => 1);
|
|
my $nwrite = syswrite(STDOUT, $opts{sockdir} . "\n");
|
|
# write error so client will retry
|
|
exit if ($nwrite < length($opts{sockdir}) + 1);
|
|
local $SIG{ALRM} = sub {die};
|
|
alarm $opts{timeout};
|
|
# client must ack sockdir before spawning daemon
|
|
my $ok = read_oob(\*STDIN);
|
|
alarm 0;
|
|
# client did not receive sockdir so will retry
|
|
exit if ($ok ne 'OK');
|
|
|
|
################
|
|
#### daemon ####
|
|
################
|
|
if (!fork) {
|
|
$self = 3;
|
|
# detach process
|
|
close STDIN;
|
|
close STDOUT;
|
|
close STDERR;
|
|
setsid;
|
|
open(STDIN, "</dev/null");
|
|
open(STDOUT, ">/dev/null");
|
|
open(STDERR, ">/dev/null");
|
|
# create stdout/stderr sockets
|
|
foreach (1, 2) {
|
|
$sock[$_] = IO::Socket::UNIX->new(
|
|
Listen => 5,
|
|
Local => "$opts{sockdir}/sock.$_",
|
|
Proto => 'tcp',
|
|
);
|
|
};
|
|
# temporarily forget sockdir to avoid some later processing
|
|
$opts{sockdir0} = $opts{sockdir};
|
|
delete $opts{sockdir};
|
|
$sel->add(@sock[1,2]);
|
|
}
|
|
} else {
|
|
# socket should already exist
|
|
$opts{timeout} = 0;
|
|
}
|
|
|
|
if (!$opts{sockdir}) {
|
|
if (!$opts{remote}) {
|
|
###############
|
|
#### local ####
|
|
###############
|
|
spawn_remote();
|
|
} else {
|
|
################
|
|
#### daemon ####
|
|
################
|
|
init_cmd();
|
|
}
|
|
}
|
|
|
|
if ($opts{sockdir}) {
|
|
###############
|
|
#### local ####
|
|
###############
|
|
################
|
|
#### remote ####
|
|
################
|
|
foreach my $fh (\*STDIN, \*STDOUT, \*STDERR) {
|
|
$fh->autoflush(1);
|
|
binmode $fh;
|
|
}
|
|
$sel->add(\*STDIN);
|
|
@caller = (\*STDIN, \*STDOUT, \*STDERR);
|
|
}
|
|
|
|
if ($opts{remote} && $opts{sockdir}) {
|
|
################
|
|
#### remote ####
|
|
################
|
|
$self = 2;
|
|
if ($opts{timeout}) {
|
|
# wait for daemon stderr socket to exist
|
|
eval {
|
|
local $SIG{ALRM} = sub {die};
|
|
alarm $opts{timeout};
|
|
sleep 1 while ($opts{timeout} && ! -S "$opts{sockdir}/sock.2");
|
|
};
|
|
alarm 0;
|
|
}
|
|
# connect to stderr socket first
|
|
foreach (2, 1) {
|
|
$child[$_] = IO::Socket::UNIX->new(
|
|
Peer => "$opts{sockdir}/sock.$_",
|
|
);
|
|
}
|
|
if (!defined $child[1] || !defined $child[2]) {
|
|
# can't connect to daemon so send eof to local and exit
|
|
my $rc = $caller[1]->syswrite("X\n");
|
|
exit;
|
|
}
|
|
# stdin/stdout use same bidirectional socket
|
|
$child[0] = $child[1];
|
|
init_child();
|
|
# associate sockets with this retry session
|
|
$child[1]->syswrite("$opts{try}\n");
|
|
$child[2]->syswrite("$opts{try}\n");
|
|
}
|
|
|
|
#TODO: catch C-c, etc. and shutdown
|
|
|
|
# finacks required to exit
|
|
my %ack = $self == 3 ? (1 => 1, 2 => 1) : ();
|
|
my @fhs;
|
|
#TODO: this loop busy waits when have writable data that cannot be
|
|
# written yet
|
|
SELECT: while ((@fhs = $sel->can_read(.1)) ||
|
|
(max(map {length($bufs[$_]) - $ibufs[$_]} (0..2)) > 0) ||
|
|
($self == 3 && (scalar(%ack) || !waitpid($pid, WNOHANG))) ||
|
|
($self == 2 && -S "$opts{sockdir}/sock.1") ||
|
|
($self == 1)) {
|
|
my $flush;
|
|
my $remove;
|
|
if (!scalar(@fhs)) {
|
|
# flush remaining buffer contents
|
|
foreach (0..2) {
|
|
push(@fhs, $_ ? $child[$_] : $caller[$_])
|
|
if (length($bufs[$_]) > $ibufs[$_]);
|
|
}
|
|
$flush = 1;
|
|
}
|
|
if ($self == 1 && !scalar(@fhs) && waitpid($pid, WNOHANG)) {
|
|
# remote has exited or died
|
|
last SELECT if (spawn_remote());
|
|
}
|
|
#TODO: 3 has to detect that command has successfully exited
|
|
foreach my $fh (@fhs) {
|
|
# ignore if no longer an input
|
|
next if (!$flush && $remove && !$sel->exists($fh));
|
|
################################
|
|
#### daemon socket handling ####
|
|
################################
|
|
if ($fh == $sock[1]) {
|
|
# socket connect from remote to daemon (stdin/stdout)
|
|
my $sock = $fh->accept;
|
|
my $try = read_oob($sock);
|
|
if (!defined $try || $try < $opts{try}) {
|
|
# remote has connected out of order
|
|
#TODO: is something else needed here?
|
|
close $sock;
|
|
next;
|
|
}
|
|
$opts{try} = $try;
|
|
# remove previous remote
|
|
if ($sel->exists($caller[0])) {
|
|
$sel->remove($caller[0]);
|
|
$remove = 1;
|
|
}
|
|
close $caller[0] if ($caller[0]);
|
|
$caller[0] = $sock;
|
|
$caller[1] = $sock;
|
|
$sel->add($caller[0]);
|
|
# send number of bytes daemon has read
|
|
my $rc = $caller[1]->syswrite($rbytes[0] . "\n");
|
|
#TODO: error handling on write
|
|
# read number of bytes read by remote
|
|
my $oob = read_oob($caller[0]);
|
|
if (!defined $oob) {
|
|
close $sock;
|
|
next;
|
|
}
|
|
my @nbytes = (0, split(/,/, $oob));
|
|
foreach my $i (1, 2) {
|
|
if ($nbytes[$i] < $rbytes[$i]) {
|
|
# send the bytes that were never received by local
|
|
$ibufs[$i] = length($bufs[$i]) - ($rbytes[$i] - $nbytes[$i]);
|
|
if ($ibufs[$i] < 0) {
|
|
#TODO: can't recover since don't have bytes anymore
|
|
}
|
|
}
|
|
}
|
|
next;
|
|
} elsif ($fh == $sock[2]) {
|
|
# socket connect from remote to daemon (stderr)
|
|
my $sock = $fh->accept;
|
|
# read retry number
|
|
my $try = read_oob($sock);
|
|
if (!defined $try || $try < $opts{try}) {
|
|
# remote has connected out of order
|
|
#TODO: is something else needed here?
|
|
close $sock;
|
|
} else {
|
|
$opts{try} = $try;
|
|
# remove previous remote
|
|
if ($sel->exists($caller[2])) {
|
|
$sel->remove($caller[2]);
|
|
$remove = 1;
|
|
}
|
|
close $caller[2] if ($caller[2]);
|
|
$caller[2] = $sock;
|
|
# add out-of-band channel to report stream 1/2 eof success
|
|
$sel->add($caller[2]);
|
|
}
|
|
next;
|
|
} elsif ($fh == $caller[2]) {
|
|
################################
|
|
#### daemon finack handling ####
|
|
################################
|
|
my $i = read_oob($fh);
|
|
if (!defined $i) {
|
|
# socket broken, so wait for respawn
|
|
if ($sel->exists($caller[0])) {
|
|
$sel->remove($caller[0]);
|
|
$remove = 1;
|
|
}
|
|
if ($sel->exists($caller[2])) {
|
|
$sel->remove($caller[2]);
|
|
$remove = 1;
|
|
}
|
|
close $caller[0] if ($caller[0]);
|
|
close $caller[2] if ($caller[2]);
|
|
next;
|
|
}
|
|
if ($i == 1 && $eof[0]) {
|
|
# close stream 0 as it could not be closed before finack
|
|
if ($sel->exists($caller[0])) {
|
|
$sel->remove($caller[0]);
|
|
$remove = 1;
|
|
}
|
|
close $caller[0];
|
|
}
|
|
delete $ack{$i};
|
|
if (!scalar(%ack)) {
|
|
# all finacks have been received so close stream 2 sock
|
|
if ($sel->exists($fh)) {
|
|
$sel->remove($fh);
|
|
$remove = 1;
|
|
}
|
|
close $fh;
|
|
}
|
|
next;
|
|
}
|
|
#################################
|
|
#### read from input channel ####
|
|
#################################
|
|
my $i = $fh == $caller[0] ? 0 : ($fh == $child[1] ? 1 : 2);
|
|
# do not read additional data if writer too far behind
|
|
$flush = 1 if (length($bufs[$i]) - $ibufs[$i] > BUF_SIZE);
|
|
my $nread;
|
|
$nread = $fh->sysread(my $buf, IO_SIZE) if (!$flush);
|
|
my $ofh = $i ? $caller[$i] : $child[$i];
|
|
$rbytes[$i] += $nread;
|
|
$bufs[$i] .= $buf;
|
|
if (!$eof[$i] && substr($bufs[$i], -EOFFOE_LEN()) eq EOFFOE) {
|
|
# eof marker has been received as final buffer content
|
|
$eof[$i] = 1;
|
|
}
|
|
if (defined $nread && $nread == 0) {
|
|
# end of stream
|
|
$sel->remove($fh);
|
|
$remove = 1;
|
|
if (!$flush && ($self == 3 && $i || $self == 1 && !$i)) {
|
|
# original non-sshim local/remote command
|
|
$eof[$i] = 1;
|
|
$bufs[$i] .= EOFFOE;
|
|
$rbytes[$i] += EOFFOE_LEN;
|
|
# do not close stderr sock since used for finack
|
|
close $fh if ($self != 3 || $i != 2 && $eof[0] && !$ack{1});
|
|
} elsif ($self == 1 && $i && !$eof[$i]) {
|
|
# respawn remote (ssh channel has likely been broken)
|
|
last SELECT if (spawn_remote());
|
|
next SELECT;
|
|
} elsif ($self == 2 && $i && !$eof[$i]) {
|
|
# 3 closed stream without eof so got connection from different 2
|
|
last SELECT;
|
|
}
|
|
}
|
|
|
|
#################################
|
|
#### write to output channel ####
|
|
#################################
|
|
if ($ofh && length($bufs[$i]) > $ibufs[$i]) {
|
|
# forward current buffer to corresponding output channel
|
|
my $size = $flush ? ~0 : IO_SIZE;
|
|
# do not forward eof to local/remote command
|
|
#TODO: does this create termination problem elsewhere in code?
|
|
# i.e. can never write full buffer to 1:1/2 or 3:0
|
|
$size = min($size, length($bufs[$i]) - $ibufs[$i] - EOFFOE_LEN)
|
|
if ($eof[$i] && ($self == 1 && $i || $self == 3 && !$i));
|
|
my $nwrite = $ofh->syswrite(substr($bufs[$i], $ibufs[$i], $size));
|
|
if (!defined $nwrite) {
|
|
if ($self == 1 && $pipe && $i && !$eof[0]) {
|
|
#TODO: at this point, 1/2 should be short-circuited since
|
|
# nothing can or should be done and may take more resources
|
|
# than normal (e.g. pipe to head)
|
|
$pipe = 0;
|
|
$eof[0] = 1;
|
|
$bufs[0] .= EOFFOE;
|
|
$rbytes[0] += EOFFOE_LEN;
|
|
#TODO: this is never flushed out to 2/3
|
|
}
|
|
# remote socket or cmd broken, let local respawn remote
|
|
last SELECT if ($self == 2);
|
|
# unable to write to output channel
|
|
if ($self == 1 && $fh == $caller[0]) {
|
|
# respawn remote (ssh channel has likely been broken)
|
|
last SELECT if (spawn_remote());
|
|
next SELECT;
|
|
} elsif ($self == 3) {
|
|
if (!$eof[$i]) {
|
|
# socket broken, so wait for respawn
|
|
if ($sel->exists($caller[0])) {
|
|
$sel->remove($caller[0]);
|
|
$remove = 1;
|
|
}
|
|
if ($sel->exists($caller[2])) {
|
|
$sel->remove($caller[2]);
|
|
$remove = 1;
|
|
}
|
|
close $caller[0] if ($caller[0]);
|
|
close $caller[2] if ($caller[2]);
|
|
}
|
|
# sleep to avoid busy wait on unforwardable but ready fh
|
|
sleep 1;
|
|
}
|
|
}
|
|
# only save BUF_SIZE bytes in buffer
|
|
$ibufs[$i] += $nwrite;
|
|
$ibufs[$i] += EOFFOE_LEN
|
|
if ($eof[$i] && ($self == 1 && $i || $self == 3 && !$i));
|
|
if ($self == 2 && $i && $eof[$i] && $ibufs[$i] >= length($bufs[$i])) {
|
|
# send finack
|
|
$child[2]->syswrite("$i\n");
|
|
}
|
|
if ($self == 3 && $i == 0 && $eof[0] &&
|
|
length($bufs[0]) - $ibufs[0] >= 0) {
|
|
close $child[0];
|
|
}
|
|
if (length($bufs[$i]) >= 3 * BUF_SIZE) {
|
|
# truncate buffer when grows too large
|
|
$bufs[$i] = substr($bufs[$i], BUF_SIZE());
|
|
if ($ibufs[$i] < BUF_SIZE) {
|
|
#TODO: losing data - no way to recover
|
|
} else {
|
|
$ibufs[$i] -= BUF_SIZE;
|
|
};
|
|
}
|
|
} elsif ($flush) {
|
|
# sleep to avoid busy wait on unforwardable but ready fh
|
|
sleep 1;
|
|
}
|
|
|
|
if (min(@eof) && min(map {length($bufs[$_]) - $ibufs[$_]} (0..2)) >= 0) {
|
|
# exit loop if eof received, no data to flush, and no acks left
|
|
last SELECT if (scalar(%ack) == 0);
|
|
}
|
|
}
|
|
}
|
|
#TODO: prevent final busy wait in error scenarios??
|
|
|
|
if ($opts{sockdir0}) {
|
|
# clean up temporary files
|
|
foreach (1, 2) {
|
|
close $sock[$_];
|
|
unlink "$opts{sockdir0}/sock.$_";
|
|
}
|
|
rmdir $opts{sockdir0};
|
|
}
|
|
|
|
if ($pid) {
|
|
kill_child();
|
|
# exit code is meaningless because it's not the original cmd
|
|
exit;
|
|
}
|
|
|
|
####################
|
|
#### kill_child ####
|
|
####################
|
|
sub kill_child {
|
|
return if (!$pid);
|
|
# killing via TERM can prevent child from flushing buffers
|
|
foreach my $fh (@child) {
|
|
close $fh;
|
|
}
|
|
#kill('TERM', $pid);
|
|
waitpid($pid, 0);
|
|
}
|
|
|
|
####################
|
|
#### init_child ####
|
|
####################
|
|
sub init_child {
|
|
# set up child file handles
|
|
foreach my $fh (@child) {
|
|
$fh->autoflush(1);
|
|
binmode $fh;
|
|
}
|
|
$sel->add(@child[1,2]);
|
|
}
|
|
|
|
##################
|
|
#### init_cmd ####
|
|
##################
|
|
sub init_cmd {
|
|
foreach my $fh (@child) {
|
|
$sel->remove($fh) if ($sel->exists($fh));
|
|
}
|
|
foreach my $fh (@child) {
|
|
close $fh;
|
|
}
|
|
my ($in, $out, $err);
|
|
$err = gensym;
|
|
# spawn given command
|
|
# stdin/out/err must not be closed or open3 has bug where handles do
|
|
# not function properly
|
|
$pid = open3($in, $out, $err, @ARGV);
|
|
#TODO: error handling
|
|
@child = ($in, $out, $err);
|
|
init_child();
|
|
}
|
|
|
|
##################
|
|
#### read_oob ####
|
|
##################
|
|
sub read_oob {
|
|
my $fh = shift;
|
|
my ($c, $recv);
|
|
# read a character at a time until newline received
|
|
$recv .= $c while (sysread($fh, $c, 1) && ord($c) != 10);
|
|
return ord($c) == 10 ? $recv : undef;
|
|
}
|
|
|
|
######################
|
|
#### spawn_remote ####
|
|
######################
|
|
sub spawn_remote {
|
|
# use --try option to prevent race conditions between multiple remotes
|
|
$opts{try}++;
|
|
if ($opts{try} == 1) {
|
|
splice(@ARGV, $iargv, 0, "--try=$opts{try}");
|
|
} else {
|
|
splice(@ARGV, $iargv - 1, 1, "--try=$opts{try}");
|
|
}
|
|
my $bytes;
|
|
my $retry = 5;
|
|
while ((!defined $opts{sockdir} || !defined $bytes) && $retry--) {
|
|
# spawn remote
|
|
kill_child();
|
|
#TODO: use timeout for connection?
|
|
init_cmd();
|
|
if (!$opts{sockdir}) {
|
|
# read sockdir
|
|
eval {
|
|
local $SIG{ALRM} = sub {die};
|
|
alarm $opts{timeout};
|
|
$opts{sockdir} = read_oob($child[1]);
|
|
};
|
|
alarm 0;
|
|
next if (!defined $opts{sockdir});
|
|
# all invocations of remote after first will use --sockdir option
|
|
splice(@ARGV, $iargv - 1, 0, "--sockdir=$opts{sockdir}")
|
|
if ($opts{sockdir});
|
|
# ack receipt of sockdir
|
|
$child[0]->syswrite("OK\n");
|
|
}
|
|
# send number of bytes local has read
|
|
my $oob = "$rbytes[1],$rbytes[2]\n";
|
|
my $nwrite = $child[0]->syswrite($oob);
|
|
$bytes = read_oob($child[1]) if ($nwrite == length($oob));
|
|
sleep 1 if (!defined $bytes);
|
|
}
|
|
return -1 if ($bytes eq 'X');
|
|
if ($bytes < $rbytes[0]) {
|
|
# send the bytes that were never received by daemon
|
|
$ibufs[0] = length($bufs[0]) - ($rbytes[0] - $bytes);
|
|
if ($ibufs[0] < 0) {
|
|
#TODO: can't recover since don't have bytes anymore
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|