mosh-server: Support timeouts on lost connectivity to network client.
Closes #690.
This commit is contained in:
@@ -82,6 +82,43 @@ Locale-related environment variable to try as part of a fallback
|
||||
environment, if the startup environment does not specify a character
|
||||
set of UTF-8.
|
||||
|
||||
.SH ENVIRONMENT VARIABLES
|
||||
These variables allow server-side configuration of Mosh's behavior.
|
||||
They may be set by administrators in system login/rc files,
|
||||
/etc/login.conf, or similar mechanisms, or users in their shell's
|
||||
login/rc files. \fBmosh-server\fP passes these variables to the login
|
||||
session and shell that it starts, but changing them there will have no
|
||||
effect.
|
||||
|
||||
.TP
|
||||
.B MOSH_SERVER_NETWORK_TMOUT
|
||||
If this variable is set to a positive integer number, it specifies how
|
||||
long (in seconds) \fBmosh-server\fP will wait to receive an update from the
|
||||
client before exiting. Since \fPmosh\fP is very useful for mobile
|
||||
clients with intermittent operation and connectivity, we suggest
|
||||
setting this variable to a high value, such as 604800 (one week) or
|
||||
2592000 (30 days). Otherwise, \fBmosh-server\fP will wait
|
||||
indefinitely for a client to reappear. This variable is somewhat
|
||||
similar to the \fBTMOUT\fP variable found in many Bourne shells.
|
||||
However, it is not a login-session inactivity timeout; it only applies
|
||||
to network connectivity.
|
||||
|
||||
.TP
|
||||
.B MOSH_SERVER_SIGNAL_TMOUT
|
||||
If this variable is set to a positive integer number, it specifies how
|
||||
long (in seconds) \fBmosh-server\fP will ignore SIGUSR1 while waiting
|
||||
to receive an update from the client. Otherwise, \fBSIGUSR1\fP will
|
||||
always terminate \fBmosh-server\fP. Users and administrators may
|
||||
implement scripts to clean up disconnected Mosh sessions. With this
|
||||
variable set, a user or administrator can issue
|
||||
|
||||
.nf
|
||||
$ pkill -SIGUSR1 mosh-server
|
||||
.fi
|
||||
|
||||
to kill disconnected sessions without killing connected login
|
||||
sessions.
|
||||
|
||||
.SH EXAMPLE
|
||||
|
||||
.nf
|
||||
|
||||
@@ -92,6 +92,13 @@ command to run server helper on remote machine (default: "mosh-server")
|
||||
The server helper is unprivileged and can be installed in the user's
|
||||
home directory.
|
||||
|
||||
This option can be used to set environment variables for the server by
|
||||
using the
|
||||
.BR env (1)
|
||||
command to wrap the actual server command. See
|
||||
.BR mosh-server (1)
|
||||
for available environment variables.
|
||||
|
||||
.TP
|
||||
.B \-\-ssh=\fICOMMAND\fP
|
||||
OpenSSH command to remotely execute mosh-server on remote machine (default: "ssh")
|
||||
|
||||
@@ -266,6 +266,8 @@ if ( (not defined $colors)
|
||||
$colors = 0;
|
||||
}
|
||||
|
||||
$ENV{ 'MOSH_CLIENT_PID' } = $$; # We don't support this, but it's useful for test and debug.
|
||||
|
||||
my $pid = open(my $pipe, "-|");
|
||||
die "$0: fork: $!\n" unless ( defined $pid );
|
||||
if ( $pid == 0 ) { # child
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+114
@@ -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
@@ -0,0 +1 @@
|
||||
server-network-timeout.test
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user