diff --git a/man/mosh-server.1 b/man/mosh-server.1 index 39fbb3451..ae4eca8dc 100644 --- a/man/mosh-server.1 +++ b/man/mosh-server.1 @@ -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 diff --git a/man/mosh.1 b/man/mosh.1 index d5b7e6493..4cd5daa8d 100644 --- a/man/mosh.1 +++ b/man/mosh.1 @@ -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") diff --git a/scripts/mosh.pl b/scripts/mosh.pl index 2ef5a8655..77bf32c1f 100755 --- a/scripts/mosh.pl +++ b/scripts/mosh.pl @@ -266,6 +266,8 @@ sub predict_check { $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 diff --git a/src/frontend/mosh-server.cc b/src/frontend/mosh-server.cc index 42c8655d2..e0aff825a 100644 --- a/src/frontend/mosh-server.cc +++ b/src/frontend/mosh-server.cc @@ -54,6 +54,7 @@ #include #include #include +#include #ifdef HAVE_UTMPX_H #include @@ -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( network_timeout ) * 1000; + const uint64_t network_signaled_timeout_ms = static_cast( 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(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; } diff --git a/src/tests/Makefile.am b/src/tests/Makefile.am index 7b3f952cc..f1445726f 100644 --- a/src/tests/Makefile.am +++ b/src/tests/Makefile.am @@ -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) diff --git a/src/tests/e2e-test-server b/src/tests/e2e-test-server index c24a8db9a..caeaafc3a 100755 --- a/src/tests/e2e-test-server +++ b/src/tests/e2e-test-server @@ -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 diff --git a/src/tests/server-network-timeout.test b/src/tests/server-network-timeout.test new file mode 100755 index 000000000..54cb1dbdf --- /dev/null +++ b/src/tests/server-network-timeout.test @@ -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 diff --git a/src/tests/server-signal-timeout.test b/src/tests/server-signal-timeout.test new file mode 120000 index 000000000..116a85ca9 --- /dev/null +++ b/src/tests/server-signal-timeout.test @@ -0,0 +1 @@ +server-network-timeout.test \ No newline at end of file diff --git a/src/util/select.cc b/src/util/select.cc index 083716140..8ac1fa073 100644 --- a/src/util/select.cc +++ b/src/util/select.cc @@ -43,5 +43,4 @@ void Select::handle_signal( int signum ) Select &sel = get_instance(); sel.got_signal[ signum ] = 1; - sel.got_any_signal = 1; } diff --git a/src/util/select.h b/src/util/select.h index a9394aaae..ca8a2de31 100644 --- a/src/util/select.h +++ b/src/util/select.h @@ -58,8 +58,6 @@ class Select { 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 @@ class Select { 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 @@ class Select { 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 @@ class Select { /* 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;