diff --git a/scripts/mosh.pl b/scripts/mosh.pl index 56e96d76e..249bbc370 100755 --- a/scripts/mosh.pl +++ b/scripts/mosh.pl @@ -350,121 +350,135 @@ sub predict_check { $userhost = "$user$ip"; } -my $pid = open(my $pipe, "-|"); -die "$0: fork: $!\n" unless ( defined $pid ); -if ( $pid == 0 ) { # child - open(STDERR, ">&STDOUT") or die; +# Construct server exec arguments. +my @sshopts = ( '-n' ); +if ($ssh_pty) { + push @sshopts, '-tt'; +} - my @sshopts = ( '-n' ); - if ($ssh_pty) { - push @sshopts, '-tt'; +my $ssh_connection = ""; +if ( $use_remote_ip eq 'remote' ) { + # Ask the server for its IP. The user's shell may not be + # Posix-compatible so invoke sh explicitly. + $ssh_connection = "sh -c " . + shell_quote ( '[ -n "$SSH_CONNECTION" ] && printf "\nMOSH SSH_CONNECTION %s\n" "$SSH_CONNECTION"' ) . + " ; "; + # Only with 'remote', we may need to tell SSH which protocol to use. + if ( $family eq 'inet' ) { + push @sshopts, '-4'; + } elsif ( $family eq 'inet6' ) { + push @sshopts, '-6'; } +} +my @server = ( 'new' ); - my $ssh_connection = ""; - if ( $use_remote_ip eq 'remote' ) { - # Ask the server for its IP. The user's shell may not be - # Posix-compatible so invoke sh explicitly. - $ssh_connection = "sh -c " . - shell_quote ( '[ -n "$SSH_CONNECTION" ] && printf "\nMOSH SSH_CONNECTION %s\n" "$SSH_CONNECTION"' ) . - " ; "; - # Only with 'remote', we may need to tell SSH which protocol to use. - if ( $family eq 'inet' ) { - push @sshopts, '-4'; - } elsif ( $family eq 'inet6' ) { - push @sshopts, '-6'; - } - } - my @server = ( 'new' ); +push @server, ( '-c', $colors ); - push @server, ( '-c', $colors ); +push @server, @bind_arguments; - push @server, @bind_arguments; +if ( defined $port_request ) { + push @server, ( '-p', $port_request ); +} - if ( defined $port_request ) { - push @server, ( '-p', $port_request ); - } +for ( &locale_vars ) { + push @server, ( '-l', $_ ); +} - for ( &locale_vars ) { - push @server, ( '-l', $_ ); - } +if ( scalar @command > 0 ) { + push @server, '--', @command; +} + +if ( $use_remote_ip eq 'proxy' ) { + my $quoted_proxy_command = shell_quote( $0, "--family=$family" ); + push @sshopts, ( '-S', 'none', '-o', "ProxyCommand=$quoted_proxy_command --fake-proxy -- %h %p" ); +} +my @exec_argv = ( @ssh, @sshopts, $userhost, '--', $ssh_connection . "$server " . shell_quote( @server ) ); + +# Override command line for local execution. +if ( defined( $localhost )) { + @exec_argv = ( "$server " . shell_quote( @server ) ); +} + +my $pid = open(my $pipe, "-|"); +die "$0: fork: $!\n" unless ( defined $pid ); +if ( $pid == 0 ) { # child + open(STDERR, ">&STDOUT") or die; - if ( scalar @command > 0 ) { - push @server, '--', @command; + if ( !defined( $localhost) && $use_remote_ip eq 'proxy' ) { + # Non-standard shells and broken shrc files cause the ssh + # proxy to break mysteriously. + $ENV{ 'SHELL' } = '/bin/sh'; } if ( defined( $localhost )) { delete $ENV{ 'SSH_CONNECTION' }; chdir; # $HOME print "MOSH IP ${userhost}\n"; - exec( "$server " . shell_quote( @server ) ); - die "Cannot exec $server: $!\n"; - } - if ( $use_remote_ip eq 'proxy' ) { - # Non-standard shells and broken shrc files cause the ssh - # proxy to break mysteriously. - $ENV{ 'SHELL' } = '/bin/sh'; - my $quoted_proxy_command = shell_quote( $0, "--family=$family" ); - push @sshopts, ( '-S', 'none', '-o', "ProxyCommand=$quoted_proxy_command --fake-proxy -- %h %p" ); } - my @exec_argv = ( @ssh, @sshopts, $userhost, '--', $ssh_connection . "$server " . shell_quote( @server ) ); + exec @exec_argv; - die "Cannot exec ssh: $!\n"; -} else { # parent - my ( $sship, $port, $key ); - my $bad_udp_port_warning = 0; - LINE: while ( <$pipe> ) { - chomp; - if ( m{^MOSH IP } ) { - if ( defined $ip ) { - die "$0 error: detected attempt to redefine MOSH IP.\n"; - } - ( $ip ) = m{^MOSH IP (\S+)\s*$} or die "Bad MOSH IP string: $_\n"; - } elsif ( m{^MOSH SSH_CONNECTION } ) { - my @words = split; - if ( scalar @words == 6 ) { - $sship = $words[4]; - } else { - die "Bad MOSH SSH_CONNECTION string: $_\n"; - } - } elsif ( m{^MOSH CONNECT } ) { - if ( ( $port, $key ) = m{^MOSH CONNECT (\d+?) ([A-Za-z0-9/+]{22})\s*$} ) { - last LINE; - } else { - die "Bad MOSH CONNECT string: $_\n"; - } + die "Cannot exec child: $!\n"; +} +# parent +my ( $sship, $port, $key ); +my $bad_udp_port_warning = 0; +LINE: while ( <$pipe> ) { + chomp; + if ( m{^MOSH IP } ) { + if ( defined $ip ) { + die "$0 error: detected attempt to redefine MOSH IP.\n"; + } + ( $ip ) = m{^MOSH IP (\S+)\s*$} or die "Bad MOSH IP string: $_\n"; + } elsif ( m{^MOSH SSH_CONNECTION } ) { + my @words = split; + if ( scalar @words == 6 ) { + $sship = $words[4]; } else { - if ( defined $port_request and $port_request =~ m{:} and m{Bad UDP port} ) { - $bad_udp_port_warning = 1; - } - print "$_\n"; + die "Bad MOSH SSH_CONNECTION string: $_\n"; } - } - waitpid $pid, 0; - close $pipe; - - if ( not defined $ip ) { - if ( defined $sship ) { - warn "$0: Using remote IP address ${sship} from \$SSH_CONNECTION for hostname ${userhost}\n"; - $ip = $sship; + } elsif ( m{^MOSH CONNECT } ) { + if ( ( $port, $key ) = m{^MOSH CONNECT (\d+?) ([A-Za-z0-9/+]{22})\s*$} ) { + last LINE; } else { - die "$0: Did not find remote IP address (is SSH ProxyCommand disabled?).\n"; + die "Bad MOSH CONNECT string: $_\n"; } + } else { + if ( defined $port_request and $port_request =~ m{:} and m{Bad UDP port} ) { + $bad_udp_port_warning = 1; + } + print "$_\n"; } +} +if ( not close $pipe ) { + if ( $! == 0 ) { + die_on_exitstatus($?, "server command", shell_quote(@exec_argv)); + } else { + die("$0: error closing server pipe: $!\n") + } +} - if ( not defined $key or not defined $port ) { - if ( $bad_udp_port_warning ) { - die "$0: Server does not support UDP port range option.\n"; - } - die "$0: Did not find mosh server startup message. (Have you installed mosh on your server?)\n"; +if ( not defined $ip ) { + if ( defined $sship ) { + warn "$0: Using remote IP address ${sship} from \$SSH_CONNECTION for hostname ${userhost}\n"; + $ip = $sship; + } else { + die "$0: Did not find remote IP address (is SSH ProxyCommand disabled?).\n"; } +} - # Now start real mosh client - $ENV{ 'MOSH_KEY' } = $key; - $ENV{ 'MOSH_PREDICTION_DISPLAY' } = $predict; - $ENV{ 'MOSH_NO_TERM_INIT' } = '1' if !$term_init; - exec {$client} ("$client", "-# @cmdline |", $ip, $port); +if ( not defined $key or not defined $port ) { + if ( $bad_udp_port_warning ) { + die "$0: Server does not support UDP port range option.\n"; + } + die "$0: Did not find mosh server startup message. (Have you installed mosh on your server?)\n"; } +# Now start real mosh client +$ENV{ 'MOSH_KEY' } = $key; +$ENV{ 'MOSH_PREDICTION_DISPLAY' } = $predict; +$ENV{ 'MOSH_NO_TERM_INIT' } = '1' if !$term_init; +exec {$client} ("$client", "-# @cmdline |", $ip, $port); + sub shell_quote { join ' ', map {(my $a = $_) =~ s/'/'\\''/g; "'$a'"} @_ } sub locale_vars { @@ -536,3 +550,20 @@ sub resolvename { } return @res; } + +sub die_on_exitstatus { + my ($exitstatus, $what_command, $exec_string) = @_; + + if (POSIX::WIFSIGNALED($exitstatus)) { + my $termsig = POSIX::WTERMSIG($exitstatus); + die("$0: $what_command exited on signal $termsig: $exec_string\n" ); + } + if (!POSIX::WIFEXITED($exitstatus)) { + die("$0: $what_command unexpectedly terminated with exit status $exitstatus: $exec_string\n"); + } + my $exitcode = POSIX::WEXITSTATUS($exitstatus); + if ($exitcode != 0) { + die("$0: $what_command failed with exitstatus $exitcode: $exec_string\n"); + } + return; +} diff --git a/src/tests/Makefile.am b/src/tests/Makefile.am index 4863af61c..322842a81 100644 --- a/src/tests/Makefile.am +++ b/src/tests/Makefile.am @@ -31,6 +31,7 @@ displaytests = \ pty-deadlock.test \ repeat.test \ repeat-with-input.test \ + server-failure.test \ server-network-timeout.test \ server-signal-timeout.test \ window-resize.test \ diff --git a/src/tests/e2e-test b/src/tests/e2e-test index c0e9d27d0..ebfcfa69f 100755 --- a/src/tests/e2e-test +++ b/src/tests/e2e-test @@ -237,21 +237,21 @@ for run in $server_tests; do if [ "$server_rv" -ne 0 ]; then test_error "server harness exited with status %s\n" "$server_rv" fi - fi - if [ "${run}" != "direct" ]; then - # Check for "round-trip" failures - if grep -q "round-trip Instruction verification failed" "${test_dir}/${run}.server.stderr"; then - test_error "Round-trip Instruction verification failed on server during %s\n" "$run" - fi - # Check for 0-timeout select() issue - if egrep -q "(polls, rate limiting|consecutive polls)" "${test_dir}/${run}.server.stderr"; then - if [ "osx" != "${TRAVIS_OS_NAME}" ]; then - test_error "select() with zero timeout called too often on server during %s\n" "$run" + if [ "${run}" != "direct" ]; then + # Check for "round-trip" failures + if grep -q "round-trip Instruction verification failed" "${test_dir}/${run}.server.stderr"; then + test_error "Round-trip Instruction verification failed on server during %s\n" "$run" + fi + # Check for 0-timeout select() issue + if egrep -q "(polls, rate limiting|consecutive polls)" "${test_dir}/${run}.server.stderr"; then + if [ "osx" != "${TRAVIS_OS_NAME}" ]; then + test_error "select() with zero timeout called too often on server during %s\n" "$run" + fi + fi + # Check for assert() + if egrep -q "assertion.*failed" "${test_dir}/${run}.server.stderr"; then + test_error "assertion during %s\n" "$run" fi - fi - # Check for assert() - if egrep -q "assertion.*failed" "${test_dir}/${run}.server.stderr"; then - test_error "assertion during %s\n" "$run" fi fi # XXX We'd also like to check for "target state Instruction diff --git a/src/tests/server-failure.test b/src/tests/server-failure.test new file mode 100755 index 000000000..be6ec5150 --- /dev/null +++ b/src/tests/server-failure.test @@ -0,0 +1,69 @@ +#!/bin/sh + +# +# Run mosh with a bad server command, check that it reports this usefully. +# + +# shellcheck source=e2e-test-subrs +. "$(dirname "$0")/e2e-test-subrs" +PATH=$PATH:.:$srcdir +# Top-level wrapper. +if [ $# -eq 0 ]; then + e2e-test "$0" baseline client server mosh-args post + exit +fi + +# OK, we have arguments, we're one of the test hooks. +if [ $# -lt 1 ]; then + fail "bad arguments %s\n" "$@" +fi + +# The mosh session test command is never run. +baseline() +{ + printf "@@@ done\n" +} + +# Return inverted exitstatus from mosh-client. +client() +{ + ! "$@" + exit +} + +# Add a do-nothing server command, to disable the standard harness +# checks in e2e-test. +server() +{ + exit 0 +} + +# Make mosh.pl fail with a bad server. +mosh_args() +{ + printf "%s\n" "--server false" +} + +# Check for correct reporting of that failure. +post() +{ + if ! grep 'server command failed with exitstatus' "$(basename "$0").d/baseline.tmux.log"; then + exit 1 + fi +} + +case $1 in + baseline) + baseline;; + client) + shift + client "$@";; + mosh-args) + mosh_args;; + post) + post;; + server) + server;; + *) + fail "unknown test argument %s\n" "$1";; +esac