Short Use fork+exec (recommended), or use piped open, or drop -t
pseudo-terminal
There is a lot going on here. You start ssh
, which needs to deal with (pseudo-) terminal business, with streams being manipulated -- and for a backgrounded process. It's this last bit that makes it tricky. I get the same behavior as you do.
Here are three solutions/workarounds which work for me. I recommend the first one.
The point of the &
in the command is that your script gets the control back right away. We can nicely achieve that right in the script. That is far cleaner, and does not depend on system details. So drop &
and put the (ssh) process in the background yourself.
use warnings;
use strict;
my $cmd = 'ssh -t machine command >stdout.log 2>error.log';
my $pid = fork // die "Can't fork: $!";
if ($pid == 0) { # child
exec($cmd);
die "exec shouldn't have returned: $! ";
}
# Parent. Child became $cmd, which went its own way, never to return.
# Rest of your code, executed right away ...
The error.log
will likely contain the line Connection to machine closed.
(it does for me). A basic explanation of fork+exec is at the end, if needed.
The following are workarounds which may have problems and may not work everywhere, but they achieve what is needed in my tests.
If command
doesn't need the controlling tty drop -t
. The redirection and &
still work for me.
Trick the STDIN
problem by starting the (ssh) process using piped open
my $cmd = 'ssh -t machine command >stdout.log 2>error.log &';
my $pid = open my $fh, '|-', $cmd // die "Can't fork: $!;
# your other code ...
close $fh;
This 'opens' (forks) a process that executes $cmd
so that $fh
is its STDIN
, and our script can feed it input if needed. So we provide the STDIN
and ssh's pseudo-terminal has no problem with it. If the command doesn't need input you can ignore that and the rest works as needed.
This is a very clean way to fork another process and pipe to/from its STDIN
or STDOUT
, see tutorial perlopentut, reference open, and for more detail Safe Pipe Opens in perlipc. However, in this case I'd rather go with fork+exec above.
A basic note on fork
-ing in this case
The fork creates a new process and returns its ID to the parent, and 0 to the child. (It's about the only call that returns twice.) This new process is a clone of the parent, except that for that process the $pid
is zero, while for the parent it's a (large) integer. (There are some other far smaller differences.)
So the child does enter the next if ($pid == 0) { }
block while the parent does not. This way we get to separate child and parent, so each can run the code dedicated to them. We get parallel execution (in principle, and also mostly in practice).
Inside the if
block, the child process is replaced altogether by the program executed by exec, in this case by your ssh
command. The exec
simply never returns (unless the call itself fails). The parent continues to run after the if
, executing normally any code that is there without blocking.
This way we spin off another program, while your program can continue with its business without waiting for the other to finish.