July 21, 2007

Non-cooked mode input problems with piping over SSH

Posted in Coding, input redirection, Linux, Software, ssh at 8:46 pm by mj

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.