mosh-server: Support timeouts on lost connectivity to network client.

Closes #690.
This commit is contained in:
John Hood
2015-10-18 16:27:31 -04:00
parent 4b8444988e
commit b742e958b6
10 changed files with 253 additions and 18 deletions
+73 -8
View File
@@ -54,6 +54,7 @@
#include <netdb.h>
#include <time.h>
#include <sys/stat.h>
#include <inttypes.h>
#ifdef HAVE_UTMPX_H
#include <utmpx.h>
@@ -93,7 +94,9 @@ typedef Network::Transport< Terminal::Complete, Network::UserStream > ServerConn
static void serve( int host_fd,
Terminal::Complete &terminal,
ServerConnection &network );
ServerConnection &network,
long network_timeout,
long network_signaled_timeout );
static int run_server( const char *desired_ip, const char *desired_port,
const string &command_path, char *command_argv[],
@@ -340,6 +343,34 @@ int main( int argc, char *argv[] )
static int run_server( const char *desired_ip, const char *desired_port,
const string &command_path, char *command_argv[],
const int colors, bool verbose, bool with_motd ) {
/* get network idle timeout */
long network_timeout = 0;
char *timeout_envar = getenv( "MOSH_SERVER_NETWORK_TMOUT" );
if ( timeout_envar && *timeout_envar ) {
errno = 0;
char *endptr;
network_timeout = strtol( timeout_envar, &endptr, 10 );
if ( *endptr != '\0' || ( network_timeout == 0 && errno == EINVAL ) ) {
fprintf( stderr, "MOSH_SERVER_NETWORK_TMOUT not a valid integer, ignoring\n" );
} else if ( network_timeout < 0 ) {
fprintf( stderr, "MOSH_SERVER_NETWORK_TMOUT is negative, ignoring\n" );
network_timeout = 0;
}
}
/* get network signaled idle timeout */
long network_signaled_timeout = 0;
char *signal_envar = getenv( "MOSH_SERVER_SIGNAL_TMOUT" );
if ( signal_envar && *signal_envar ) {
errno = 0;
char *endptr;
network_signaled_timeout = strtol( signal_envar, &endptr, 10 );
if ( *endptr != '\0' || ( network_signaled_timeout == 0 && errno == EINVAL ) ) {
fprintf( stderr, "MOSH_SERVER_SIGNAL_TMOUT not a valid integer, ignoring\n" );
} else if ( network_signaled_timeout < 0 ) {
fprintf( stderr, "MOSH_SERVER_SIGNAL_TMOUT is negative, ignoring\n" );
network_signaled_timeout = 0;
}
}
/* get initial window size */
struct winsize window_size;
if ( ioctl( STDIN_FILENO, TIOCGWINSZ, &window_size ) < 0 ||
@@ -505,7 +536,7 @@ static int run_server( const char *desired_ip, const char *desired_port,
#endif
try {
serve( master, terminal, *network );
serve( master, terminal, *network, network_timeout, network_signaled_timeout );
} catch ( const Network::NetworkException &e ) {
fprintf( stderr, "Network exception: %s\n",
e.what() );
@@ -531,12 +562,16 @@ static int run_server( const char *desired_ip, const char *desired_port,
return 0;
}
static void serve( int host_fd, Terminal::Complete &terminal, ServerConnection &network )
static void serve( int host_fd, Terminal::Complete &terminal, ServerConnection &network, long network_timeout, long network_signaled_timeout )
{
/* scale timeouts */
const uint64_t network_timeout_ms = static_cast<uint64_t>( network_timeout ) * 1000;
const uint64_t network_signaled_timeout_ms = static_cast<uint64_t>( network_signaled_timeout ) * 1000;
/* prepare to poll for events */
Select &sel = Select::get_instance();
sel.add_signal( SIGTERM );
sel.add_signal( SIGINT );
sel.add_signal( SIGUSR1 );
uint64_t last_remote_num = network.get_remote_state_num();
@@ -549,14 +584,31 @@ static void serve( int host_fd, Terminal::Complete &terminal, ServerConnection &
while ( 1 ) {
try {
static const uint64_t timeout_if_no_client = 60000;
int timeout = INT_MAX;
uint64_t now = Network::timestamp();
const int timeout_if_no_client = 60000;
int timeout = min( network.wait_time(), terminal.wait_time( now ) );
timeout = min( timeout, network.wait_time() );
timeout = min( timeout, terminal.wait_time( now ) );
if ( (!network.get_remote_state_num())
|| network.shutdown_in_progress() ) {
timeout = min( timeout, 5000 );
}
/*
* The server goes completely asleep if it has no remote peer.
* We may want to wake up sooner.
*/
if ( network_timeout_ms ) {
int64_t network_sleep = network_timeout_ms -
( now - network.get_latest_remote_state().timestamp );
if ( network_sleep < 0 ) {
network_sleep = 0;
} else if ( network_sleep > INT_MAX ) {
/* 24 days might be too soon. That's OK. */
network_sleep = INT_MAX;
}
timeout = min( timeout, static_cast<int>(network_sleep) );
}
/* poll for events */
sel.clear_fds();
@@ -679,7 +731,20 @@ static void serve( int host_fd, Terminal::Complete &terminal, ServerConnection &
}
}
if ( sel.any_signal() ) {
bool idle_shutdown = false;
if ( network_timeout_ms &&
network_timeout_ms <= time_since_remote_state ) {
idle_shutdown = true;
fprintf( stderr, "Network idle for %" PRIu64 " seconds.\n", time_since_remote_state / 1000 );
}
if ( sel.signal( SIGUSR1 ) ) {
if ( !network_signaled_timeout_ms || network_signaled_timeout_ms <= time_since_remote_state ) {
idle_shutdown = true;
fprintf( stderr, "Network idle for %"PRIu64" seconds when SIGUSR1 received\n", time_since_remote_state / 1000 );
}
}
if ( sel.any_signal() || idle_shutdown ) {
/* shutdown signal */
if ( network.has_remote_addr() && (!network.shutdown_in_progress()) ) {
network.start_shutdown();
@@ -736,8 +801,8 @@ static void serve( int host_fd, Terminal::Complete &terminal, ServerConnection &
}
if ( !network.get_remote_state_num()
&& time_since_remote_state >= uint64_t( timeout_if_no_client ) ) {
fprintf( stderr, "No connection within %d seconds.\n",
&& time_since_remote_state >= timeout_if_no_client ) {
fprintf( stderr, "No connection within %" PRIu64 " seconds.\n",
timeout_if_no_client / 1000 );
break;
}
+4 -2
View File
@@ -12,9 +12,11 @@ displaytests = \
emulation-80th-column.test \
emulation-back-tab.test \
emulation-multiline-scroll.test \
window-resize.test \
server-network-timeout.test \
server-signal-timeout.test \
unicode-combine-fallback-assert.test \
unicode-later-combining.test
unicode-later-combining.test \
window-resize.test
check_PROGRAMS = ocb-aes encrypt-decrypt base64
TESTS = ocb-aes encrypt-decrypt base64 $(displaytests)
+3
View File
@@ -5,6 +5,8 @@
# then captures screen with `tmux capture-pane`. Captures exitstatus
# of both and returns appropriate errors.
#
export MOSH_SERVER_PID=$PPID
if [ $# -lt 2 ]; then
printf "not enough args\n" >&2
exit 99
@@ -12,6 +14,7 @@ fi
testname=$1
shift
rm -f $testname.capture $testname.exitstatus
trap ":" TERM HUP QUIT # If the session closes on us, let the test we're running drive.
trap 'rv=$?; echo $rv > $testname.exitstatus; exit $rv' EXIT
# check for tmux
if [ -z "$TMUX_PANE" ]; then
+114
View File
@@ -0,0 +1,114 @@
#!/bin/sh
#
# This test checks for operation of the MOSH_SERVER_NETWORK_TIMEOUT variable.
# It does this by
# * setting the variable
# * killing the client (and its network traffic)
# * waiting server-side, for the server to die
# If it is killed, the test is successful.
# If it survives that long and the server is still around, the test fails.
# The client waits a bit longer than the server so that status can be collected
# properly.
#
TIMEOUT=10
fail()
{
printf "$@" 2>&1
exit 99
}
PATH=$PATH:.:$srcdir
# Top-level wrapper.
if [ $# -eq 0 ]; then
e2e-test $0 client baseline
exit
fi
# OK, we have arguments, we're one of the test hooks.
client()
{
case "$myname" in
server-network-timeout)
export MOSH_SERVER_NETWORK_TMOUT=$TIMEOUT;;
server-signal-timeout)
export MOSH_SERVER_SIGNAL_TMOUT=$TIMEOUT;;
*)
fail "unexpected test name %s\n" "$myname"
esac
shift
eval "$@"
# The client may be murdered. We need to expect that...
retval=$?
case $retval in
0|1)
fail "mosh-client had a normal exit\n";; # test condition failed
137)
# Aha, signal 9. Wait.
sleep $(( $TIMEOUT + 12 ))
exit 0
;;
*)
fail "unknown client wrapper failure, retval=%d\n" $retval
;;
esac
fail "client wrapper shouldnt get here\n"
}
baseline()
{
# check for our wonderful variable
if [ -z "$MOSH_SERVER_NETWORK_TMOUT" -a -z "$MOSH_SERVER_SIGNAL_TMOUT" ]; then
env
fail "Variable unset\n"
fi
# check for our client
if [ -z "$MOSH_CLIENT_PID" ]; then
env
fail "Client pid unavailable\n"
fi
if ! kill -0 $MOSH_CLIENT_PID; then
fail "mosh client is unexpectedly missing\n"
fi
# Set up for good return and cleanup on being killed
trap "echo got killed >&2; sleep 1; exit 0" SIGHUP SIGTERM
sleep 1
# Kill the client, to stop network traffic.
kill -KILL $MOSH_CLIENT_PID
case "$myname" in
server-network-timeout)
# Just wait. This is the hardest part.
sleep $(( $TIMEOUT + 7 ))
;;
server-signal-timeout)
# Wait for the timeout to expire.
sleep $(( $TIMEOUT + 2 ))
# Tell the server to go away.
kill -USR1 $MOSH_SERVER_PID
sleep 5
;;
*)
fail "unexpected test name %s\n" "$myname"
esac
# If we're still alive and the server is too, the test failed.
# XXX the server is getting killed and we're getting here anyway.
# Exit with error only if server is still around.
! kill -0 $MOSH_SERVER_PID
exit
}
myname="$(basename $0 .test)"
case $1 in
baseline|variant)
baseline;;
client)
client "$@";;
*)
fail "unknown test argument %s\n" $1;;
esac
+1
View File
@@ -0,0 +1 @@
server-network-timeout.test
-1
View File
@@ -43,5 +43,4 @@ void Select::handle_signal( int signum )
Select &sel = get_instance();
sel.got_signal[ signum ] = 1;
sel.got_any_signal = 1;
}
+12 -7
View File
@@ -58,8 +58,6 @@ public:
private:
Select()
: max_fd( -1 )
, got_any_signal( 0 )
/* These initializations are not used; they are just
here to appease -Weffc++. */
, all_fds( dummy_fd_set )
@@ -124,7 +122,6 @@ public:
memcpy( &read_fds, &all_fds, sizeof( read_fds ) );
memcpy( &error_fds, &all_fds, sizeof( error_fds ) );
clear_got_signal();
got_any_signal = 0;
#ifdef HAVE_PSELECT
struct timespec ts;
@@ -185,16 +182,25 @@ public:
return FD_ISSET( fd, &error_fds );
}
bool signal( int signum ) const
/* This method consumes a signal notification. */
bool signal( int signum )
{
fatal_assert( signum >= 0 );
fatal_assert( signum <= MAX_SIGNAL_NUMBER );
return got_signal[ signum ];
/* XXX This requires a guard against concurrent signals. */
bool rv = got_signal[ signum ];
got_signal[ signum ] = 0;
return rv;
}
/* This method does not consume signal notifications. */
bool any_signal( void ) const
{
return got_any_signal;
bool rv = false;
for (int i = 0; i < MAX_SIGNAL_NUMBER; i++) {
rv |= got_signal[ i ];
}
return rv;
}
private:
@@ -206,7 +212,6 @@ private:
/* We assume writes to these ints are atomic, though we also try to mask out
concurrent signal handlers. */
int got_any_signal;
int got_signal[ MAX_SIGNAL_NUMBER + 1 ];
fd_set all_fds, read_fds, error_fds;