July 21, 2007
Non-cooked mode input problems with piping over SSH
Thought I’d share this, since what I thought was a 2-hour Saturday early morning side project went horribly awry.
Consider this Perl code snippet:
use Term::ReadKey; ReadMode('cbreak'); while (1) { my $key = ReadKey(-1); if (defined($key)) { print "Got key $key\\n"; last; } } ReadMode('normal');
Simple enough: it does nothing until you press a key. This idiom might be used, for example, with non-blocking reads while you’re processing another file, as below:
use Term::ReadKey; use IO::Select; my $handle = IO::File->new("tail -f /some/file |"); my $selectable = IO::Select->new(); $selectable->add($handle); my ($tailCommandPid) = getChildPids(); ReadMode('cbreak'); while (1) { my $key = ReadKey(-1); if (defined($key)) { print "Got key $key\\n"; last; } my @ready = $selectable->can_read(1); foreach my $h (@ready) { my $line = <$h>; print $line if defined($line); } } ReadMode('normal'); $selectable->remove($handle); kill 9, $tailCommandPid; $handle->close();
This also works. It continually echoes the lines it reads, until you press a key.
But what if you want to read the lines from a remote shell? Can we replace the line
my $handle = IO::File->new("tail -f /some/file |");
with
my $handle = IO::File->new("ssh someServer tail -f /some/file |");
and be on our merry way?
Well, no. In this case, your input on STDIN
is completely ignored, unless you switch back to plain-old cooked mode.
Or is it really ignored? Maybe ssh is using your input for its own nefarious purposes. Makes perfect sense if your program is but a mediary between your user and an interactive remote shell.
Turns out, ssh accepts -f
as an argument to force it into the background. Will that work?
my $handle = IO::File->new("ssh -f someServer tail -f /some/file |");
Success! But now we’re killing the wrong process at the end.
By forcing itself into the background, ssh is really daemonizing itself: its parent PID (on Linux) is 1. Your program will exit, but you’ll leave ssh happily running in the background, unless you result to less-than-safe tricks with ps
and grep
.
Further down in the man page, however, and we find the trick: -n
keeps ssh running as a normal child process, but does not read from STDIN
:
my $handle = IO::File->new("ssh -n someServer tail -f /some/file |");
And this, my friends, is a mistake I will never make again. And neither will you.