Separate modules by subdirectory
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
AM_CPPFLAGS = -I$(srcdir)/../statesync -I$(srcdir)/../terminal -I$(srcdir)/../network -I$(srcdir)/../crypto -I$(builddir)/../protobufs -I$(srcdir)/../util
|
||||
AM_CXXFLAGS = --std=c++0x -pedantic -Werror -Wall -Wextra -Weffc++ -fno-default-inline -pipe
|
||||
LIBS = `pkg-config --libs protobuf-lite`
|
||||
LDADD = ../crypto/libmoshcrypto.a ../network/libmoshnetwork.a ../statesync/libmoshstatesync.a ../terminal/libmoshterminal.a ../util/libmoshutil.a ../protobufs/libmoshprotos.a -lutil -lrt -lm
|
||||
|
||||
bin_PROGRAMS = mosh-client mosh-server
|
||||
|
||||
mosh_client_SOURCES = mosh-client.cc stmclient.cc stmclient.h terminaloverlay.cc terminaloverlay.h
|
||||
mosh_server_SOURCES = mosh-server.cc
|
||||
@@ -0,0 +1,58 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "stmclient.h"
|
||||
#include "crypto.h"
|
||||
|
||||
int main( int argc, char *argv[] )
|
||||
{
|
||||
/* Get arguments */
|
||||
char *ip;
|
||||
int port;
|
||||
|
||||
if ( argc != 3 ) {
|
||||
fprintf( stderr, "Usage: %s IP PORT\n", argv[ 0 ] );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
ip = argv[ 1 ];
|
||||
port = myatoi( argv[ 2 ] );
|
||||
|
||||
/* Read key from environment */
|
||||
char *env_key = getenv( "MOSH_KEY" );
|
||||
if ( env_key == NULL ) {
|
||||
fprintf( stderr, "MOSH_KEY environment variable not found.\n" );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
char *key = strdup( env_key );
|
||||
if ( key == NULL ) {
|
||||
perror( "strdup" );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
if ( unsetenv( "MOSH_KEY" ) < 0 ) {
|
||||
perror( "unsetenv" );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
/* Adopt native locale */
|
||||
if ( NULL == setlocale( LC_ALL, "" ) ) {
|
||||
perror( "setlocale" );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
STMClient client( ip, port, key );
|
||||
|
||||
client.init();
|
||||
|
||||
client.main();
|
||||
|
||||
client.shutdown();
|
||||
|
||||
printf( "\n[mosh is exiting.]\n" );
|
||||
|
||||
free( key );
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
#include <locale.h>
|
||||
#include <string.h>
|
||||
#include <langinfo.h>
|
||||
#include <termios.h>
|
||||
#include <unistd.h>
|
||||
#include <stdio.h>
|
||||
#include <pty.h>
|
||||
#include <stdlib.h>
|
||||
#include <poll.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/types.h>
|
||||
#include <pwd.h>
|
||||
#include <typeinfo>
|
||||
#include <signal.h>
|
||||
#include <sys/signalfd.h>
|
||||
|
||||
#include "completeterminal.h"
|
||||
#include "swrite.h"
|
||||
#include "user.h"
|
||||
|
||||
#include "networktransport.cc"
|
||||
|
||||
void serve( int host_fd, const char *desired_ip );
|
||||
|
||||
using namespace std;
|
||||
|
||||
int main( int argc, char *argv[] )
|
||||
{
|
||||
char *desired_ip = NULL;
|
||||
if ( argc == 1 ) {
|
||||
desired_ip = NULL;
|
||||
} else if ( argc == 2 ) {
|
||||
desired_ip = argv[ 1 ];
|
||||
} else {
|
||||
fprintf( stderr, "Usage: %s [LOCALADDR]\n", argv[ 0 ] );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
int master;
|
||||
struct termios child_termios;
|
||||
|
||||
/* Adopt implementation locale */
|
||||
if ( NULL == setlocale( LC_ALL, "" ) ) {
|
||||
perror( "setlocale" );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
/* Verify locale calls for UTF-8 */
|
||||
if ( strcmp( nl_langinfo( CODESET ), "UTF-8" ) != 0 ) {
|
||||
fprintf( stderr, "mosh requires a UTF-8 locale.\n" );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
/* Verify terminal configuration */
|
||||
if ( tcgetattr( STDIN_FILENO, &child_termios ) < 0 ) {
|
||||
perror( "tcgetattr" );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
if ( !(child_termios.c_iflag & IUTF8) ) {
|
||||
/* SSH should also convey IUTF8 across connection. */
|
||||
// fprintf( stderr, "Warning: Locale is UTF-8 but termios IUTF8 flag not set. Setting IUTF8 flag.\n" );
|
||||
child_termios.c_iflag |= IUTF8;
|
||||
}
|
||||
|
||||
/* Fork child process */
|
||||
pid_t child = forkpty( &master, NULL, &child_termios, NULL );
|
||||
|
||||
if ( child == -1 ) {
|
||||
perror( "forkpty" );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
if ( child == 0 ) {
|
||||
/* child */
|
||||
if ( setenv( "TERM", "xterm", true ) < 0 ) {
|
||||
perror( "setenv" );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
/* ask ncurses to send UTF-8 instead of ISO 2022 for line-drawing chars */
|
||||
if ( setenv( "NCURSES_NO_UTF8_ACS", "1", true ) < 0 ) {
|
||||
perror( "setenv" );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
/* clear STY environment variable so GNU screen regards us as top level */
|
||||
if ( unsetenv( "STY" ) < 0 ) {
|
||||
perror( "unsetenv" );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
/* get shell name */
|
||||
struct passwd *pw = getpwuid( geteuid() );
|
||||
if ( pw == NULL ) {
|
||||
perror( "getpwuid" );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
char *my_argv[ 2 ];
|
||||
my_argv[ 0 ] = strdup( pw->pw_shell );
|
||||
assert( my_argv[ 0 ] );
|
||||
my_argv[ 1 ] = NULL;
|
||||
|
||||
if ( execve( pw->pw_shell, my_argv, environ ) < 0 ) {
|
||||
perror( "execve" );
|
||||
exit( 1 );
|
||||
}
|
||||
exit( 0 );
|
||||
} else {
|
||||
/* parent */
|
||||
serve( master, desired_ip );
|
||||
if ( close( master ) < 0 ) {
|
||||
perror( "close" );
|
||||
exit( 1 );
|
||||
}
|
||||
}
|
||||
|
||||
printf( "\n[mosh-server is exiting.]\n" );
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void serve( int host_fd, const char *desired_ip )
|
||||
{
|
||||
/* establish fd for shutdown signals */
|
||||
sigset_t signal_mask;
|
||||
|
||||
assert( sigemptyset( &signal_mask ) == 0 );
|
||||
assert( sigaddset( &signal_mask, SIGTERM ) == 0 );
|
||||
assert( sigaddset( &signal_mask, SIGINT ) == 0 );
|
||||
|
||||
sigset_t signals_to_block = signal_mask;
|
||||
|
||||
assert( sigaddset( &signals_to_block, SIGHUP ) == 0 );
|
||||
assert( sigaddset( &signals_to_block, SIGPIPE ) == 0 );
|
||||
|
||||
/* don't let signals kill us */
|
||||
assert( sigprocmask( SIG_BLOCK, &signals_to_block, NULL ) == 0 );
|
||||
|
||||
int shutdown_signal_fd = signalfd( -1, &signal_mask, 0 );
|
||||
if ( shutdown_signal_fd < 0 ) {
|
||||
perror( "signalfd" );
|
||||
return;
|
||||
}
|
||||
|
||||
/* get initial window size */
|
||||
struct winsize window_size;
|
||||
if ( ioctl( STDIN_FILENO, TIOCGWINSZ, &window_size ) < 0 ) {
|
||||
perror( "ioctl TIOCGWINSZ" );
|
||||
return;
|
||||
}
|
||||
|
||||
/* tell child process */
|
||||
if ( ioctl( host_fd, TIOCSWINSZ, &window_size ) < 0 ) {
|
||||
perror( "ioctl TIOCSWINSZ" );
|
||||
return;
|
||||
}
|
||||
|
||||
/* open parser and terminal */
|
||||
Terminal::Complete terminal( window_size.ws_col, window_size.ws_row );
|
||||
|
||||
/* open network */
|
||||
Network::UserStream blank;
|
||||
Network::Transport< Terminal::Complete, Network::UserStream > network( terminal, blank, desired_ip );
|
||||
|
||||
/* network.set_verbose(); */
|
||||
|
||||
printf( "MOSH CONNECT %d %s\n", network.port(), network.get_key().c_str() );
|
||||
fflush( stdout );
|
||||
|
||||
/* detach from terminal */
|
||||
pid_t the_pid = fork();
|
||||
if ( the_pid < 0 ) {
|
||||
perror( "fork" );
|
||||
} else if ( the_pid > 0 ) {
|
||||
_exit( 0 );
|
||||
}
|
||||
|
||||
fprintf( stderr, "[mosh-server detached, pid=%d.]\n", (int)getpid() );
|
||||
|
||||
/* prepare to poll for events */
|
||||
struct pollfd pollfds[ 3 ];
|
||||
|
||||
pollfds[ 0 ].fd = network.fd();
|
||||
pollfds[ 0 ].events = POLLIN;
|
||||
|
||||
pollfds[ 1 ].fd = host_fd;
|
||||
pollfds[ 1 ].events = POLLIN;
|
||||
|
||||
pollfds[ 2 ].fd = shutdown_signal_fd;
|
||||
pollfds[ 2 ].events = POLLIN;
|
||||
|
||||
uint64_t last_remote_num = network.get_remote_state_num();
|
||||
|
||||
while ( 1 ) {
|
||||
try {
|
||||
int active_fds = poll( pollfds, 3, network.wait_time() );
|
||||
if ( active_fds < 0 ) {
|
||||
perror( "poll" );
|
||||
break;
|
||||
}
|
||||
|
||||
if ( pollfds[ 0 ].revents & POLLIN ) {
|
||||
/* packet received from the network */
|
||||
network.recv();
|
||||
|
||||
/* is new user input available for the terminal? */
|
||||
if ( network.get_remote_state_num() != last_remote_num ) {
|
||||
string terminal_to_host;
|
||||
|
||||
Network::UserStream us;
|
||||
us.apply_string( network.get_remote_diff() );
|
||||
/* apply userstream to terminal */
|
||||
for ( size_t i = 0; i < us.size(); i++ ) {
|
||||
terminal_to_host += terminal.act( us.get_action( i ) );
|
||||
if ( typeid( *us.get_action( i ) ) == typeid( Parser::Resize ) ) {
|
||||
/* tell child process of resize */
|
||||
const Parser::Resize *res = static_cast<const Parser::Resize *>( us.get_action( i ) );
|
||||
window_size.ws_col = res->width;
|
||||
window_size.ws_row = res->height;
|
||||
if ( ioctl( host_fd, TIOCSWINSZ, &window_size ) < 0 ) {
|
||||
perror( "ioctl TIOCSWINSZ" );
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* update client with new state of terminal */
|
||||
if ( !network.shutdown_in_progress() ) {
|
||||
network.set_current_state( terminal );
|
||||
}
|
||||
|
||||
/* write any writeback octets back to the host */
|
||||
if ( swrite( host_fd, terminal_to_host.c_str(), terminal_to_host.length() ) < 0 ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( pollfds[ 1 ].revents & POLLIN ) {
|
||||
/* input from the host needs to be fed to the terminal */
|
||||
const int buf_size = 16384;
|
||||
char buf[ buf_size ];
|
||||
|
||||
/* fill buffer if possible */
|
||||
ssize_t bytes_read = read( pollfds[ 1 ].fd, buf, buf_size );
|
||||
if ( bytes_read == 0 ) { /* EOF */
|
||||
return;
|
||||
} else if ( bytes_read < 0 ) {
|
||||
perror( "read" );
|
||||
return;
|
||||
}
|
||||
|
||||
string terminal_to_host = terminal.act( string( buf, bytes_read ) );
|
||||
|
||||
/* update client with new state of terminal */
|
||||
if ( !network.shutdown_in_progress() ) {
|
||||
network.set_current_state( terminal );
|
||||
}
|
||||
|
||||
/* write any writeback octets back to the host */
|
||||
if ( swrite( host_fd, terminal_to_host.c_str(), terminal_to_host.length() ) < 0 ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( pollfds[ 2 ].revents & POLLIN ) {
|
||||
/* shutdown signal */
|
||||
struct signalfd_siginfo the_siginfo;
|
||||
ssize_t bytes_read = read( pollfds[ 2 ].fd, &the_siginfo, sizeof( the_siginfo ) );
|
||||
if ( bytes_read == 0 ) {
|
||||
break;
|
||||
} else if ( bytes_read < 0 ) {
|
||||
perror( "read" );
|
||||
break;
|
||||
}
|
||||
|
||||
if ( network.attached() && (!network.shutdown_in_progress()) ) {
|
||||
network.start_shutdown();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( (pollfds[ 0 ].revents)
|
||||
& (POLLERR | POLLHUP | POLLNVAL) ) {
|
||||
/* network problem */
|
||||
break;
|
||||
}
|
||||
|
||||
if ( (pollfds[ 1 ].revents)
|
||||
& (POLLERR | POLLHUP | POLLNVAL) ) {
|
||||
/* host problem */
|
||||
if ( network.attached() ) {
|
||||
network.start_shutdown();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* quit if our shutdown has been acknowledged */
|
||||
if ( network.shutdown_in_progress() && network.shutdown_acknowledged() ) {
|
||||
break;
|
||||
}
|
||||
|
||||
/* quit after shutdown acknowledgement timeout */
|
||||
if ( network.shutdown_in_progress() && network.shutdown_ack_timed_out() ) {
|
||||
break;
|
||||
}
|
||||
|
||||
/* quit if we received and acknowledged a shutdown request */
|
||||
if ( network.counterparty_shutdown_ack_sent() ) {
|
||||
break;
|
||||
}
|
||||
|
||||
network.tick();
|
||||
} catch ( Network::NetworkException e ) {
|
||||
fprintf( stderr, "%s: %s\n", e.function.c_str(), strerror( e.the_errno ) );
|
||||
sleep( 1 );
|
||||
} catch ( Crypto::CryptoException e ) {
|
||||
fprintf( stderr, "Crypto exception: %s\n", e.text.c_str() );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
#include <locale.h>
|
||||
#include <string.h>
|
||||
#include <langinfo.h>
|
||||
#include <unistd.h>
|
||||
#include <stdio.h>
|
||||
#include <pty.h>
|
||||
#include <stdlib.h>
|
||||
#include <poll.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/types.h>
|
||||
#include <pwd.h>
|
||||
#include <signal.h>
|
||||
#include <sys/signalfd.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "stmclient.h"
|
||||
#include "swrite.h"
|
||||
#include "completeterminal.h"
|
||||
#include "user.h"
|
||||
|
||||
#include "networktransport.cc"
|
||||
|
||||
void STMClient::init( void )
|
||||
{
|
||||
/* Verify locale calls for UTF-8 */
|
||||
if ( strcmp( nl_langinfo( CODESET ), "UTF-8" ) != 0 ) {
|
||||
fprintf( stderr, "mosh requires a UTF-8 locale.\n" );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
/* Verify terminal configuration */
|
||||
if ( tcgetattr( STDIN_FILENO, &saved_termios ) < 0 ) {
|
||||
perror( "tcgetattr" );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
/* Put terminal driver in raw mode */
|
||||
raw_termios = saved_termios;
|
||||
if ( !(raw_termios.c_iflag & IUTF8) ) {
|
||||
/* SSH should also convey IUTF8 across connection. */
|
||||
// fprintf( stderr, "Warning: Locale is UTF-8 but termios IUTF8 flag not set. Setting IUTF8 flag.\n" );
|
||||
raw_termios.c_iflag |= IUTF8;
|
||||
}
|
||||
|
||||
cfmakeraw( &raw_termios );
|
||||
|
||||
if ( tcsetattr( STDIN_FILENO, TCSANOW, &raw_termios ) < 0 ) {
|
||||
perror( "tcsetattr" );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
/* Put terminal in application-cursor-key mode */
|
||||
swrite( STDOUT_FILENO, Terminal::Emulator::open().c_str() );
|
||||
|
||||
/* Add our name to window title */
|
||||
overlays.set_title_prefix( wstring( L"[mosh] " ) );
|
||||
}
|
||||
|
||||
void STMClient::shutdown( void )
|
||||
{
|
||||
/* Restore screen state */
|
||||
overlays.get_notification_engine().set_notification_string( wstring( L"" ) );
|
||||
overlays.get_notification_engine().server_heard( timestamp() );
|
||||
overlays.set_title_prefix( wstring( L"" ) );
|
||||
output_new_frame();
|
||||
|
||||
/* Restore terminal and terminal-driver state */
|
||||
swrite( STDOUT_FILENO, Terminal::Emulator::close().c_str() );
|
||||
|
||||
if ( tcsetattr( STDIN_FILENO, TCSANOW, &saved_termios ) < 0 ) {
|
||||
perror( "tcsetattr" );
|
||||
exit( 1 );
|
||||
}
|
||||
}
|
||||
|
||||
void STMClient::main_init( void )
|
||||
{
|
||||
/* establish WINCH fd and start listening for signal */
|
||||
sigset_t signal_mask;
|
||||
assert( sigemptyset( &signal_mask ) == 0 );
|
||||
assert( sigaddset( &signal_mask, SIGWINCH ) == 0 );
|
||||
|
||||
/* stop "ignoring" WINCH signal */
|
||||
assert( sigprocmask( SIG_BLOCK, &signal_mask, NULL ) == 0 );
|
||||
|
||||
winch_fd = signalfd( -1, &signal_mask, 0 );
|
||||
if ( winch_fd < 0 ) {
|
||||
perror( "signalfd" );
|
||||
return;
|
||||
}
|
||||
|
||||
/* establish fd for shutdown signals */
|
||||
assert( sigemptyset( &signal_mask ) == 0 );
|
||||
assert( sigaddset( &signal_mask, SIGTERM ) == 0 );
|
||||
assert( sigaddset( &signal_mask, SIGINT ) == 0 );
|
||||
assert( sigaddset( &signal_mask, SIGHUP ) == 0 );
|
||||
assert( sigaddset( &signal_mask, SIGPIPE ) == 0 );
|
||||
assert( sigaddset( &signal_mask, SIGTSTP ) == 0 );
|
||||
assert( sigaddset( &signal_mask, SIGSTOP ) == 0 );
|
||||
assert( sigaddset( &signal_mask, SIGCONT ) == 0 );
|
||||
|
||||
/* don't let signals kill us */
|
||||
assert( sigprocmask( SIG_BLOCK, &signal_mask, NULL ) == 0 );
|
||||
|
||||
shutdown_signal_fd = signalfd( -1, &signal_mask, 0 );
|
||||
if ( shutdown_signal_fd < 0 ) {
|
||||
perror( "signalfd" );
|
||||
return;
|
||||
}
|
||||
|
||||
/* get initial window size */
|
||||
if ( ioctl( STDIN_FILENO, TIOCGWINSZ, &window_size ) < 0 ) {
|
||||
perror( "ioctl TIOCGWINSZ" );
|
||||
return;
|
||||
}
|
||||
|
||||
/* local state */
|
||||
local_framebuffer = new Terminal::Framebuffer( window_size.ws_col, window_size.ws_row );
|
||||
|
||||
/* initialize screen */
|
||||
string init = Terminal::Display::new_frame( false, *local_framebuffer, *local_framebuffer );
|
||||
swrite( STDOUT_FILENO, init.data(), init.size() );
|
||||
|
||||
/* open network */
|
||||
Network::UserStream blank;
|
||||
Terminal::Complete local_terminal( window_size.ws_col, window_size.ws_row );
|
||||
network = new Network::Transport< Network::UserStream, Terminal::Complete >( blank, local_terminal,
|
||||
key.c_str(), ip.c_str(), port );
|
||||
|
||||
network->set_send_delay( 1 ); /* minimal delay on outgoing keystrokes */
|
||||
|
||||
/* tell server the size of the terminal */
|
||||
network->get_current_state().push_back( Parser::Resize( window_size.ws_col, window_size.ws_row ) );
|
||||
}
|
||||
|
||||
void STMClient::output_new_frame( void )
|
||||
{
|
||||
/* fetch target state */
|
||||
Terminal::Framebuffer new_state( network->get_latest_remote_state().state.get_fb() );
|
||||
|
||||
/* apply local overlays */
|
||||
overlays.apply( new_state );
|
||||
|
||||
/* calculate minimal difference from where we are */
|
||||
const string diff( Terminal::Display::new_frame( !repaint_requested,
|
||||
*local_framebuffer,
|
||||
new_state ) );
|
||||
swrite( STDOUT_FILENO, diff.data(), diff.size() );
|
||||
*local_framebuffer = new_state;
|
||||
|
||||
repaint_requested = false;
|
||||
}
|
||||
|
||||
bool STMClient::process_network_input( void )
|
||||
{
|
||||
network->recv();
|
||||
|
||||
/* Now give hints to the overlays */
|
||||
overlays.get_notification_engine().server_heard( network->get_latest_remote_state().timestamp );
|
||||
|
||||
overlays.get_prediction_engine().set_local_frame_acked( network->get_sent_state_acked() );
|
||||
overlays.get_prediction_engine().set_send_interval( network->send_interval() );
|
||||
overlays.get_prediction_engine().set_local_frame_late_acked( network->get_sent_state_late_acked() );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool STMClient::process_user_input( int fd )
|
||||
{
|
||||
const int buf_size = 16384;
|
||||
char buf[ buf_size ];
|
||||
|
||||
/* fill buffer if possible */
|
||||
ssize_t bytes_read = read( fd, buf, buf_size );
|
||||
if ( bytes_read == 0 ) { /* EOF */
|
||||
return false;
|
||||
} else if ( bytes_read < 0 ) {
|
||||
perror( "read" );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( !network->shutdown_in_progress() ) {
|
||||
overlays.get_prediction_engine().set_local_frame_sent( network->get_sent_state_last() );
|
||||
|
||||
for ( int i = 0; i < bytes_read; i++ ) {
|
||||
char the_byte = buf[ i ];
|
||||
|
||||
overlays.get_prediction_engine().new_user_byte( the_byte, *local_framebuffer );
|
||||
|
||||
if ( quit_sequence_started ) {
|
||||
if ( the_byte == '.' ) { /* Quit sequence is Ctrl-^ . */
|
||||
if ( network->attached() && (!network->shutdown_in_progress()) ) {
|
||||
overlays.get_notification_engine().set_notification_string( wstring( L"Exiting on user request..." ) );
|
||||
network->start_shutdown();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else if ( the_byte == '^' ) {
|
||||
/* Emulation sequence to type Ctrl-^ is Ctrl-^ ^ */
|
||||
network->get_current_state().push_back( Parser::UserByte( 0x1E ) );
|
||||
} else {
|
||||
/* Ctrl-^ followed by anything other than . and ^ gets sent literally */
|
||||
network->get_current_state().push_back( Parser::UserByte( 0x1E ) );
|
||||
network->get_current_state().push_back( Parser::UserByte( the_byte ) );
|
||||
}
|
||||
|
||||
quit_sequence_started = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
quit_sequence_started = (the_byte == 0x1E);
|
||||
if ( quit_sequence_started ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( the_byte == 0x0C ) { /* Ctrl-L */
|
||||
repaint_requested = true;
|
||||
}
|
||||
|
||||
network->get_current_state().push_back( Parser::UserByte( the_byte ) );
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool STMClient::process_resize( void )
|
||||
{
|
||||
struct signalfd_siginfo info;
|
||||
assert( read( winch_fd, &info, sizeof( info ) ) == sizeof( info ) );
|
||||
assert( info.ssi_signo == SIGWINCH );
|
||||
|
||||
/* get new size */
|
||||
if ( ioctl( STDIN_FILENO, TIOCGWINSZ, &window_size ) < 0 ) {
|
||||
perror( "ioctl TIOCGWINSZ" );
|
||||
return false;
|
||||
}
|
||||
|
||||
/* tell remote emulator */
|
||||
Parser::Resize res( window_size.ws_col, window_size.ws_row );
|
||||
|
||||
if ( !network->shutdown_in_progress() ) {
|
||||
network->get_current_state().push_back( res );
|
||||
}
|
||||
|
||||
/* note remote emulator will probably reply with its own Resize to adjust our state */
|
||||
|
||||
/* tell prediction engine */
|
||||
overlays.get_prediction_engine().reset();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void STMClient::main( void )
|
||||
{
|
||||
/* initialize signal handling and structures */
|
||||
main_init();
|
||||
|
||||
/* prepare to poll for events */
|
||||
struct pollfd pollfds[ 4 ];
|
||||
|
||||
pollfds[ 0 ].fd = network->fd();
|
||||
pollfds[ 0 ].events = POLLIN;
|
||||
|
||||
pollfds[ 1 ].fd = STDIN_FILENO;
|
||||
pollfds[ 1 ].events = POLLIN;
|
||||
|
||||
pollfds[ 2 ].fd = winch_fd;
|
||||
pollfds[ 2 ].events = POLLIN;
|
||||
|
||||
pollfds[ 3 ].fd = shutdown_signal_fd;
|
||||
pollfds[ 3 ].events = POLLIN;
|
||||
|
||||
while ( 1 ) {
|
||||
try {
|
||||
output_new_frame();
|
||||
|
||||
int active_fds = poll( pollfds, 4, min( network->wait_time(), overlays.wait_time() ) );
|
||||
if ( active_fds < 0 ) {
|
||||
perror( "poll" );
|
||||
break;
|
||||
}
|
||||
|
||||
if ( pollfds[ 0 ].revents & POLLIN ) {
|
||||
/* packet received from the network */
|
||||
if ( !process_network_input() ) { return; }
|
||||
}
|
||||
|
||||
if ( pollfds[ 1 ].revents & POLLIN ) {
|
||||
/* input from the user needs to be fed to the network */
|
||||
if ( !process_user_input( pollfds[ 1 ].fd ) ) {
|
||||
if ( !network->attached() ) {
|
||||
break;
|
||||
} else if ( !network->shutdown_in_progress() ) {
|
||||
overlays.get_notification_engine().set_notification_string( wstring( L"Exiting..." ) );
|
||||
network->start_shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( pollfds[ 2 ].revents & POLLIN ) {
|
||||
/* resize */
|
||||
if ( !process_resize() ) { return; }
|
||||
}
|
||||
|
||||
if ( pollfds[ 3 ].revents & POLLIN ) {
|
||||
/* shutdown signal */
|
||||
struct signalfd_siginfo the_siginfo;
|
||||
ssize_t bytes_read = read( pollfds[ 3 ].fd, &the_siginfo, sizeof( the_siginfo ) );
|
||||
if ( bytes_read == 0 ) {
|
||||
break;
|
||||
} else if ( bytes_read < 0 ) {
|
||||
perror( "read" );
|
||||
break;
|
||||
}
|
||||
|
||||
if ( !network->attached() ) {
|
||||
break;
|
||||
} else if ( !network->shutdown_in_progress() ) {
|
||||
overlays.get_notification_engine().set_notification_string( wstring( L"Signal received, shutting down..." ) );
|
||||
network->start_shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
if ( (pollfds[ 0 ].revents)
|
||||
& (POLLERR | POLLHUP | POLLNVAL) ) {
|
||||
/* network problem */
|
||||
break;
|
||||
}
|
||||
|
||||
if ( (pollfds[ 1 ].revents)
|
||||
& (POLLERR | POLLHUP | POLLNVAL) ) {
|
||||
/* user problem */
|
||||
if ( !network->attached() ) {
|
||||
break;
|
||||
} else if ( !network->shutdown_in_progress() ) {
|
||||
overlays.get_notification_engine().set_notification_string( wstring( L"Exiting..." ) );
|
||||
network->start_shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/* quit if our shutdown has been acknowledged */
|
||||
if ( network->shutdown_in_progress() && network->shutdown_acknowledged() ) {
|
||||
break;
|
||||
}
|
||||
|
||||
/* quit after shutdown acknowledgement timeout */
|
||||
if ( network->shutdown_in_progress() && network->shutdown_ack_timed_out() ) {
|
||||
break;
|
||||
}
|
||||
|
||||
/* quit if we received and acknowledged a shutdown request */
|
||||
if ( network->counterparty_shutdown_ack_sent() ) {
|
||||
break;
|
||||
}
|
||||
|
||||
static const wstring connecting_notification( L"Connecting..." );
|
||||
if ( (network->get_remote_state_num() == 0) && (!network->shutdown_in_progress()) ) {
|
||||
overlays.get_notification_engine().set_notification_string( connecting_notification );
|
||||
} else if ( (network->get_remote_state_num() != 0)
|
||||
&& (overlays.get_notification_engine().get_notification_string()
|
||||
== connecting_notification) ) {
|
||||
overlays.get_notification_engine().set_notification_string( L"" );
|
||||
}
|
||||
|
||||
network->tick();
|
||||
} catch ( Network::NetworkException e ) {
|
||||
if ( !network->shutdown_in_progress() ) {
|
||||
wchar_t tmp[ 128 ];
|
||||
swprintf( tmp, 128, L"%s: %s", e.function.c_str(), strerror( e.the_errno ) );
|
||||
overlays.get_notification_engine().set_notification_string( wstring( tmp ) );
|
||||
}
|
||||
|
||||
struct timespec req;
|
||||
req.tv_sec = 0;
|
||||
req.tv_nsec = 200000000; /* 0.2 sec */
|
||||
nanosleep( &req, NULL );
|
||||
} catch ( Crypto::CryptoException e ) {
|
||||
wchar_t tmp[ 128 ];
|
||||
swprintf( tmp, 128, L"Crypto exception: %s", e.text.c_str() );
|
||||
overlays.get_notification_engine().set_notification_string( wstring( tmp ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#ifndef STM_CLIENT_HPP
|
||||
#define STM_CLIENT_HPP
|
||||
|
||||
#include <sys/ioctl.h>
|
||||
#include <termios.h>
|
||||
#include <string>
|
||||
|
||||
#include "completeterminal.h"
|
||||
#include "networktransport.h"
|
||||
#include "user.h"
|
||||
#include "terminaloverlay.h"
|
||||
|
||||
class STMClient {
|
||||
private:
|
||||
std::string ip;
|
||||
int port;
|
||||
std::string key;
|
||||
|
||||
struct termios saved_termios, raw_termios;
|
||||
|
||||
int winch_fd, shutdown_signal_fd;
|
||||
struct winsize window_size;
|
||||
|
||||
Terminal::Framebuffer *local_framebuffer;
|
||||
Overlay::OverlayManager overlays;
|
||||
Network::Transport< Network::UserStream, Terminal::Complete > *network;
|
||||
|
||||
bool repaint_requested, quit_sequence_started;
|
||||
|
||||
void main_init( void );
|
||||
bool process_network_input( void );
|
||||
bool process_user_input( int fd );
|
||||
bool process_resize( void );
|
||||
|
||||
void output_new_frame( void );
|
||||
|
||||
public:
|
||||
STMClient( const char *s_ip, int s_port, const char *s_key )
|
||||
: ip( s_ip ), port( s_port ), key( s_key ),
|
||||
saved_termios(), raw_termios(),
|
||||
winch_fd(), shutdown_signal_fd(),
|
||||
window_size(),
|
||||
local_framebuffer( NULL ),
|
||||
overlays(),
|
||||
network( NULL ),
|
||||
repaint_requested( false ),
|
||||
quit_sequence_started( false )
|
||||
{}
|
||||
|
||||
void init( void );
|
||||
void shutdown( void );
|
||||
void main( void );
|
||||
|
||||
~STMClient()
|
||||
{
|
||||
if ( local_framebuffer != NULL ) {
|
||||
delete local_framebuffer;
|
||||
}
|
||||
|
||||
if ( network != NULL ) {
|
||||
delete network;
|
||||
}
|
||||
}
|
||||
|
||||
/* unused */
|
||||
STMClient( const STMClient & );
|
||||
STMClient & operator=( const STMClient & );
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,720 @@
|
||||
#include <algorithm>
|
||||
#include <wchar.h>
|
||||
#include <list>
|
||||
#include <typeinfo>
|
||||
#include <limits.h>
|
||||
|
||||
#include "terminaloverlay.h"
|
||||
|
||||
using namespace Overlay;
|
||||
|
||||
bool ConditionalOverlay::start_clock( uint64_t local_frame_acked, uint64_t now, unsigned int send_interval )
|
||||
{
|
||||
if ( (local_frame_acked >= expiration_frame) && (expiration_time == uint64_t(-1)) ) {
|
||||
expiration_time = now + 50 + 125 * send_interval / 100;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void ConditionalOverlayCell::apply( Framebuffer &fb, uint64_t confirmed_epoch, int row, bool flag ) const
|
||||
{
|
||||
if ( (!active)
|
||||
|| (row >= fb.ds.get_height())
|
||||
|| (col >= fb.ds.get_width()) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( tentative( confirmed_epoch ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( unknown ) {
|
||||
if ( flag && ( col != fb.ds.get_width() - 1 ) ) {
|
||||
fb.get_mutable_cell( row, col )->renditions.underlined = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ( !(*(fb.get_cell( row, col )) == replacement) ) {
|
||||
if ( replacement.is_blank() && fb.get_cell( row, col )->is_blank() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
*(fb.get_mutable_cell( row, col )) = replacement;
|
||||
if ( flag ) {
|
||||
fb.get_mutable_cell( row, col )->renditions.underlined = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Validity ConditionalOverlayCell::get_validity( const Framebuffer &fb, int row,
|
||||
uint64_t sent_frame, uint64_t early_ack, uint64_t late_ack,
|
||||
uint64_t now ) const
|
||||
{
|
||||
if ( !active ) {
|
||||
return Inactive;
|
||||
}
|
||||
|
||||
if ( (row >= fb.ds.get_height())
|
||||
|| (col >= fb.ds.get_width()) ) {
|
||||
return IncorrectOrExpired;
|
||||
}
|
||||
|
||||
if ( unknown ) {
|
||||
return CorrectNoCredit;
|
||||
}
|
||||
|
||||
const Cell ¤t = *( fb.get_cell( row, col ) );
|
||||
|
||||
/* see if it hasn't been updated yet */
|
||||
if ( early_ack < expiration_frame ) {
|
||||
return Pending;
|
||||
}
|
||||
|
||||
assert( expiration_time != uint64_t(-1) );
|
||||
|
||||
if ( (late_ack >= expiration_frame)
|
||||
|| ( (sent_frame <= early_ack) && (expiration_time <= now) ) ) {
|
||||
/* special case deletion */
|
||||
if ( current.is_blank() && replacement.is_blank() ) {
|
||||
return CorrectNoCredit;
|
||||
}
|
||||
|
||||
if ( current.contents == replacement.contents ) {
|
||||
auto it = find_if( original_contents.begin(), original_contents.end(),
|
||||
[&]( const Cell &x ) { return replacement.contents == x.contents; } );
|
||||
if ( it == original_contents.end() ) {
|
||||
return Correct;
|
||||
} else {
|
||||
return CorrectNoCredit;
|
||||
}
|
||||
} else {
|
||||
return IncorrectOrExpired;
|
||||
}
|
||||
}
|
||||
|
||||
return Pending;
|
||||
}
|
||||
|
||||
Validity ConditionalCursorMove::get_validity( const Framebuffer &fb, uint64_t sent_frame, uint64_t early_ack, uint64_t late_ack, uint64_t now ) const
|
||||
{
|
||||
if ( !active ) {
|
||||
return Inactive;
|
||||
}
|
||||
|
||||
if ( (row >= fb.ds.get_height())
|
||||
|| (col >= fb.ds.get_width()) ) {
|
||||
assert( false );
|
||||
// fprintf( stderr, "Crazy cursor (%d,%d)!\n", row, col );
|
||||
return IncorrectOrExpired;
|
||||
}
|
||||
|
||||
if ( early_ack < expiration_frame ) {
|
||||
return Pending;
|
||||
}
|
||||
|
||||
assert( expiration_time != uint64_t(-1) );
|
||||
|
||||
if ( (late_ack >= expiration_frame)
|
||||
|| ( (sent_frame <= early_ack) && (expiration_time <= now) ) ) {
|
||||
if ( (fb.ds.get_cursor_col() == col)
|
||||
&& (fb.ds.get_cursor_row() == row) ) {
|
||||
return Correct;
|
||||
} else {
|
||||
return IncorrectOrExpired;
|
||||
}
|
||||
}
|
||||
|
||||
return Pending;
|
||||
}
|
||||
|
||||
void ConditionalCursorMove::apply( Framebuffer &fb, uint64_t confirmed_epoch ) const
|
||||
{
|
||||
if ( !active ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( tentative( confirmed_epoch ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
assert( row < fb.ds.get_height() );
|
||||
assert( col < fb.ds.get_width() );
|
||||
assert( !fb.ds.origin_mode );
|
||||
|
||||
fb.ds.move_row( row, false );
|
||||
fb.ds.move_col( col, false, false );
|
||||
}
|
||||
|
||||
NotificationEngine::NotificationEngine()
|
||||
: last_word_from_server( timestamp() ),
|
||||
message(),
|
||||
message_expiration( -1 )
|
||||
{}
|
||||
|
||||
void NotificationEngine::apply( Framebuffer &fb ) const
|
||||
{
|
||||
uint64_t now = timestamp();
|
||||
|
||||
bool time_expired = need_countup( now );
|
||||
|
||||
if ( message.empty() && !time_expired ) {
|
||||
return;
|
||||
}
|
||||
|
||||
assert( fb.ds.get_width() > 0 );
|
||||
assert( fb.ds.get_height() > 0 );
|
||||
|
||||
/* hide cursor if necessary */
|
||||
if ( fb.ds.get_cursor_row() == 0 ) {
|
||||
fb.ds.cursor_visible = false;
|
||||
}
|
||||
|
||||
/* draw bar across top of screen */
|
||||
Cell notification_bar( 0 );
|
||||
notification_bar.renditions.foreground_color = 37;
|
||||
notification_bar.renditions.background_color = 44;
|
||||
notification_bar.contents.push_back( 0x20 );
|
||||
|
||||
for ( int i = 0; i < fb.ds.get_width(); i++ ) {
|
||||
*(fb.get_mutable_cell( 0, i )) = notification_bar;
|
||||
}
|
||||
|
||||
/* write message */
|
||||
wchar_t tmp[ 128 ];
|
||||
|
||||
if ( message.empty() && (!time_expired) ) {
|
||||
return;
|
||||
} else if ( message.empty() && time_expired ) {
|
||||
swprintf( tmp, 128, L"mosh: Last contact %.0f seconds ago. [To quit: Ctrl-^ .]", (double)(now - last_word_from_server) / 1000.0 );
|
||||
} else if ( (!message.empty()) && (!time_expired) ) {
|
||||
swprintf( tmp, 128, L"mosh: %ls [To quit: Ctrl-^ .]", message.c_str() );
|
||||
} else {
|
||||
swprintf( tmp, 128, L"mosh: %ls (%.0f s without contact.) [To quit: Ctrl-^ .]", message.c_str(),
|
||||
(double)(now - last_word_from_server) / 1000.0 );
|
||||
}
|
||||
|
||||
wstring string_to_draw( tmp );
|
||||
|
||||
int overlay_col = 0;
|
||||
|
||||
Cell *combining_cell = fb.get_mutable_cell( 0, 0 );
|
||||
|
||||
/* We unfortunately duplicate the terminal's logic for how to render a Unicode sequence into graphemes */
|
||||
for ( wstring::const_iterator i = string_to_draw.begin(); i != string_to_draw.end(); i++ ) {
|
||||
if ( overlay_col >= fb.ds.get_width() ) {
|
||||
break;
|
||||
}
|
||||
|
||||
wchar_t ch = *i;
|
||||
int chwidth = ch == L'\0' ? -1 : wcwidth( ch );
|
||||
Cell *this_cell = nullptr;
|
||||
|
||||
switch ( chwidth ) {
|
||||
case 1: /* normal character */
|
||||
case 2: /* wide character */
|
||||
this_cell = fb.get_mutable_cell( 0, overlay_col );
|
||||
fb.reset_cell( this_cell );
|
||||
this_cell->renditions.bold = true;
|
||||
this_cell->renditions.foreground_color = 37;
|
||||
this_cell->renditions.background_color = 44;
|
||||
|
||||
this_cell->contents.push_back( ch );
|
||||
this_cell->width = chwidth;
|
||||
combining_cell = this_cell;
|
||||
|
||||
overlay_col += chwidth;
|
||||
break;
|
||||
case 0: /* combining character */
|
||||
if ( !combining_cell ) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ( combining_cell->contents.size() == 0 ) {
|
||||
assert( combining_cell->width == 1 );
|
||||
combining_cell->fallback = true;
|
||||
overlay_col++;
|
||||
}
|
||||
|
||||
if ( combining_cell->contents.size() < 16 ) {
|
||||
combining_cell->contents.push_back( ch );
|
||||
}
|
||||
break;
|
||||
case -1: /* unprintable character */
|
||||
break;
|
||||
default:
|
||||
assert( false );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void NotificationEngine::adjust_message( void )
|
||||
{
|
||||
if ( timestamp() >= message_expiration ) {
|
||||
message.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void OverlayManager::apply( Framebuffer &fb )
|
||||
{
|
||||
predictions.cull( fb );
|
||||
predictions.apply( fb );
|
||||
notifications.adjust_message();
|
||||
notifications.apply( fb );
|
||||
title.apply( fb );
|
||||
}
|
||||
|
||||
int OverlayManager::wait_time( void )
|
||||
{
|
||||
uint64_t next_expiry = INT_MAX;
|
||||
|
||||
uint64_t now = timestamp();
|
||||
|
||||
uint64_t message_delay = notifications.get_message_expiration() - now;
|
||||
|
||||
if ( message_delay < next_expiry ) {
|
||||
next_expiry = message_delay;
|
||||
}
|
||||
|
||||
if ( notifications.need_countup( now ) && ( next_expiry > 1000 ) ) {
|
||||
next_expiry = 1000;
|
||||
}
|
||||
|
||||
if ( predictions.active() && ( next_expiry > 10 ) ) {
|
||||
next_expiry = 10;
|
||||
}
|
||||
|
||||
return next_expiry;
|
||||
}
|
||||
|
||||
void TitleEngine::set_prefix( const wstring s )
|
||||
{
|
||||
prefix = deque<wchar_t>( s.begin(), s.end() );
|
||||
}
|
||||
|
||||
void ConditionalOverlayRow::apply( Framebuffer &fb, uint64_t confirmed_epoch, bool flag ) const
|
||||
{
|
||||
for_each( overlay_cells.begin(), overlay_cells.end(), [&]( const ConditionalOverlayCell &x ) { x.apply( fb, confirmed_epoch, row_num, flag ); } );
|
||||
}
|
||||
|
||||
void PredictionEngine::apply( Framebuffer &fb ) const
|
||||
{
|
||||
for_each( cursors.begin(), cursors.end(), [&]( const ConditionalCursorMove &x ) { x.apply( fb, confirmed_epoch ); } );
|
||||
|
||||
for_each( overlays.begin(), overlays.end(), [&]( const ConditionalOverlayRow &x ){ x.apply( fb, confirmed_epoch, flagging ); } );
|
||||
}
|
||||
|
||||
void PredictionEngine::kill_epoch( uint64_t epoch, const Framebuffer &fb )
|
||||
{
|
||||
cursors.remove_if( [&]( ConditionalCursorMove &x ) { return x.tentative( epoch - 1 ); } );
|
||||
|
||||
cursors.push_back( ConditionalCursorMove( local_frame_sent + 1,
|
||||
fb.ds.get_cursor_row(),
|
||||
fb.ds.get_cursor_col(),
|
||||
prediction_epoch ) );
|
||||
cursor().active = true;
|
||||
|
||||
for ( auto i = overlays.begin(); i != overlays.end(); i++ ) {
|
||||
for ( auto j = i->overlay_cells.begin(); j != i->overlay_cells.end(); j++ ) {
|
||||
if ( j->tentative( epoch - 1 ) ) {
|
||||
j->reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
become_tentative();
|
||||
}
|
||||
|
||||
void PredictionEngine::reset( void )
|
||||
{
|
||||
cursors.clear();
|
||||
overlays.clear();
|
||||
become_tentative();
|
||||
|
||||
// fprintf( stderr, "RESETTING\n" );
|
||||
}
|
||||
|
||||
void PredictionEngine::init_cursor( const Framebuffer &fb )
|
||||
{
|
||||
if ( cursors.empty() ) {
|
||||
/* initialize new cursor prediction */
|
||||
|
||||
cursors.push_back( ConditionalCursorMove( local_frame_sent + 1,
|
||||
fb.ds.get_cursor_row(),
|
||||
fb.ds.get_cursor_col(),
|
||||
prediction_epoch ) );
|
||||
|
||||
cursor().active = true;
|
||||
} else if ( cursor().tentative_until_epoch != prediction_epoch ) {
|
||||
cursors.push_back( ConditionalCursorMove( local_frame_sent + 1,
|
||||
cursor().row,
|
||||
cursor().col,
|
||||
prediction_epoch ) );
|
||||
|
||||
cursor().active = true;
|
||||
}
|
||||
}
|
||||
|
||||
void PredictionEngine::cull( const Framebuffer &fb )
|
||||
{
|
||||
uint64_t now = timestamp();
|
||||
|
||||
/* control flagging with hysteresis */
|
||||
if ( send_interval > 80 ) {
|
||||
flagging = true;
|
||||
} else if ( send_interval < 50 ) {
|
||||
flagging = false;
|
||||
}
|
||||
|
||||
/* go through cell predictions */
|
||||
|
||||
auto i = overlays.begin();
|
||||
while ( i != overlays.end() ) {
|
||||
auto inext = i;
|
||||
inext++;
|
||||
if ( (i->row_num < 0) || (i->row_num >= fb.ds.get_height()) ) {
|
||||
overlays.erase( i );
|
||||
i = inext;
|
||||
continue;
|
||||
}
|
||||
|
||||
for ( auto j = i->overlay_cells.begin(); j != i->overlay_cells.end(); j++ ) {
|
||||
if ( j->start_clock( local_frame_acked, now, send_interval ) ) {
|
||||
last_scheduled_timeout = max( last_scheduled_timeout, j->expiration_time );
|
||||
}
|
||||
switch ( j->get_validity( fb, i->row_num,
|
||||
local_frame_sent, local_frame_acked, local_frame_late_acked,
|
||||
now ) ) {
|
||||
case IncorrectOrExpired:
|
||||
if ( j->tentative( confirmed_epoch ) ) {
|
||||
|
||||
/*
|
||||
fprintf( stderr, "Bad tentative prediction in row %d, col %d (think %lc, actually %lc)\n",
|
||||
i->row_num, j->col,
|
||||
j->replacement.debug_contents(),
|
||||
fb.get_cell( i->row_num, j->col )->debug_contents()
|
||||
);
|
||||
*/
|
||||
|
||||
kill_epoch( j->tentative_until_epoch, fb );
|
||||
/*
|
||||
if ( j->display_time != uint64_t(-1) ) {
|
||||
fprintf( stderr, "TIMING %ld - %ld (TENT)\n", time(NULL), now - j->display_time );
|
||||
}
|
||||
*/
|
||||
} else {
|
||||
/*
|
||||
fprintf( stderr, "[%d=>%d] Killing prediction in row %d, col %d (think %lc, actually %lc)\n",
|
||||
(int)local_frame_acked, (int)j->expiration_frame,
|
||||
i->row_num, j->col,
|
||||
j->replacement.debug_contents(),
|
||||
fb.get_cell( i->row_num, j->col )->debug_contents() );
|
||||
*/
|
||||
/*
|
||||
if ( j->display_time != uint64_t(-1) ) {
|
||||
fprintf( stderr, "TIMING %ld - %ld\n", time(NULL), now - j->display_time );
|
||||
}
|
||||
*/
|
||||
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case Correct:
|
||||
/*
|
||||
if ( j->display_time != uint64_t(-1) ) {
|
||||
fprintf( stderr, "TIMING %ld + %ld\n", now, now - j->display_time );
|
||||
}
|
||||
*/
|
||||
|
||||
if ( j->tentative_until_epoch > confirmed_epoch ) {
|
||||
confirmed_epoch = j->tentative_until_epoch;
|
||||
|
||||
/*
|
||||
fprintf( stderr, "%lc in (%d,%d) confirms epoch %lu (predicting in epoch %lu)\n",
|
||||
j->replacement.debug_contents(), i->row_num, j->col,
|
||||
confirmed_epoch, prediction_epoch );
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
/* no break */
|
||||
case CorrectNoCredit:
|
||||
j->reset();
|
||||
|
||||
break;
|
||||
case Pending:
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
i = inext;
|
||||
}
|
||||
|
||||
/* go through cursor predictions */
|
||||
for ( auto it = cursors.begin(); it != cursors.end(); it++ ) {
|
||||
if ( it->start_clock( local_frame_acked, now, send_interval ) ) {
|
||||
last_scheduled_timeout = max( last_scheduled_timeout, it->expiration_time );
|
||||
}
|
||||
}
|
||||
|
||||
if ( !cursors.empty() ) {
|
||||
if ( cursor().get_validity( fb,
|
||||
local_frame_sent, local_frame_acked, local_frame_late_acked,
|
||||
now ) == IncorrectOrExpired ) {
|
||||
/*
|
||||
fprintf( stderr, "Sadly, we're predicting (%d,%d) vs. (%d,%d) [tau: %ld, expiration_time=%ld, now=%ld]\n",
|
||||
cursor().row, cursor().col,
|
||||
fb.ds.get_cursor_row(),
|
||||
fb.ds.get_cursor_col(),
|
||||
cursor().tentative_until_epoch,
|
||||
cursor().expiration_time,
|
||||
now );
|
||||
*/
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
cursors.remove_if( [&]( const ConditionalCursorMove &x ) {
|
||||
return (x.get_validity( fb,
|
||||
local_frame_sent, local_frame_acked, local_frame_late_acked,
|
||||
now ) != Pending); } );
|
||||
}
|
||||
|
||||
ConditionalOverlayRow & PredictionEngine::get_or_make_row( int row_num, int num_cols )
|
||||
{
|
||||
auto it = find_if( overlays.begin(), overlays.end(),
|
||||
[&]( const ConditionalOverlayRow &x ) { return x.row_num == row_num; } );
|
||||
|
||||
if ( it != overlays.end() ) {
|
||||
return *it;
|
||||
} else {
|
||||
/* make row */
|
||||
ConditionalOverlayRow r( row_num );
|
||||
r.overlay_cells.reserve( num_cols );
|
||||
for ( int i = 0; i < num_cols; i++ ) {
|
||||
r.overlay_cells.push_back( ConditionalOverlayCell( 0, i, prediction_epoch ) );
|
||||
assert( r.overlay_cells[ i ].col == i );
|
||||
}
|
||||
overlays.push_back( r );
|
||||
return overlays.back();
|
||||
}
|
||||
}
|
||||
|
||||
void PredictionEngine::new_user_byte( char the_byte, const Framebuffer &fb )
|
||||
{
|
||||
cull( fb );
|
||||
|
||||
/* translate application-mode cursor control function to ANSI cursor control sequence */
|
||||
if ( (last_byte == 0x1b)
|
||||
&& (the_byte == 'O') ) {
|
||||
the_byte = '[';
|
||||
}
|
||||
last_byte = the_byte;
|
||||
|
||||
list<Parser::Action *> actions( parser.input( the_byte ) );
|
||||
|
||||
for ( auto it = actions.begin(); it != actions.end(); it++ ) {
|
||||
Parser::Action *act = *it;
|
||||
|
||||
/*
|
||||
fprintf( stderr, "Action: %s (%lc)\n",
|
||||
act->name().c_str(), act->char_present ? act->ch : L'_' );
|
||||
*/
|
||||
|
||||
if ( typeid( *act ) == typeid( Parser::Print ) ) {
|
||||
/* make new prediction */
|
||||
|
||||
init_cursor( fb );
|
||||
|
||||
assert( act->char_present );
|
||||
|
||||
wchar_t ch = act->ch;
|
||||
/* XXX handle wide characters */
|
||||
|
||||
if ( ch == 0x7f ) { /* backspace */
|
||||
// fprintf( stderr, "Backspace.\n" );
|
||||
ConditionalOverlayRow &the_row = get_or_make_row( cursor().row, fb.ds.get_width() );
|
||||
|
||||
if ( cursor().col > 0 ) {
|
||||
cursor().col--;
|
||||
cursor().expire( local_frame_sent + 1 );
|
||||
|
||||
for ( int i = cursor().col; i < fb.ds.get_width(); i++ ) {
|
||||
ConditionalOverlayCell &cell = the_row.overlay_cells[ i ];
|
||||
|
||||
cell.reset_with_orig();
|
||||
cell.active = true;
|
||||
cell.tentative_until_epoch = prediction_epoch;
|
||||
cell.expire( local_frame_sent + 1 );
|
||||
cell.original_contents.push_back( *fb.get_cell( cursor().row, i ) );
|
||||
|
||||
if ( i + 2 < fb.ds.get_width() ) {
|
||||
ConditionalOverlayCell &next_cell = the_row.overlay_cells[ i + 1 ];
|
||||
const Cell *next_cell_actual = fb.get_cell( cursor().row, i + 1 );
|
||||
|
||||
if ( next_cell.active ) {
|
||||
if ( next_cell.unknown ) {
|
||||
cell.unknown = true;
|
||||
} else {
|
||||
cell.unknown = false;
|
||||
cell.replacement = next_cell.replacement;
|
||||
}
|
||||
} else {
|
||||
cell.unknown = false;
|
||||
cell.replacement = *next_cell_actual;
|
||||
}
|
||||
} else {
|
||||
cell.unknown = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ( (ch < 0x20) || (wcwidth( ch ) != 1) ) {
|
||||
/* unknown print */
|
||||
become_tentative();
|
||||
// fprintf( stderr, "Unknown print 0x%x\n", ch );
|
||||
} else {
|
||||
assert( cursor().row >= 0 );
|
||||
assert( cursor().col >= 0 );
|
||||
assert( cursor().row < fb.ds.get_height() );
|
||||
assert( cursor().col < fb.ds.get_width() );
|
||||
|
||||
ConditionalOverlayRow &the_row = get_or_make_row( cursor().row, fb.ds.get_width() );
|
||||
|
||||
if ( cursor().col + 1 >= fb.ds.get_width() ) {
|
||||
/* prediction in the last column is tricky */
|
||||
/* e.g., emacs will show wrap character, shell will just put the character there */
|
||||
become_tentative();
|
||||
}
|
||||
|
||||
/* do the insert */
|
||||
for ( int i = fb.ds.get_width() - 1; i > cursor().col; i-- ) {
|
||||
ConditionalOverlayCell &cell = the_row.overlay_cells[ i ];
|
||||
cell.reset_with_orig();
|
||||
cell.active = true;
|
||||
cell.tentative_until_epoch = prediction_epoch;
|
||||
cell.expire( local_frame_sent + 1 );
|
||||
cell.original_contents.push_back( *fb.get_cell( cursor().row, i ) );
|
||||
|
||||
ConditionalOverlayCell &prev_cell = the_row.overlay_cells[ i - 1 ];
|
||||
const Cell *prev_cell_actual = fb.get_cell( cursor().row, i - 1 );
|
||||
|
||||
if ( i == fb.ds.get_width() - 1 ) {
|
||||
cell.unknown = true;
|
||||
} else if ( prev_cell.active ) {
|
||||
if ( prev_cell.unknown ) {
|
||||
cell.unknown = true;
|
||||
} else {
|
||||
cell.unknown = false;
|
||||
cell.replacement = prev_cell.replacement;
|
||||
}
|
||||
} else {
|
||||
cell.unknown = false;
|
||||
cell.replacement = *prev_cell_actual;
|
||||
}
|
||||
}
|
||||
|
||||
ConditionalOverlayCell &cell = the_row.overlay_cells[ cursor().col ];
|
||||
cell.reset_with_orig();
|
||||
cell.active = true;
|
||||
cell.tentative_until_epoch = prediction_epoch;
|
||||
cell.expire( local_frame_sent + 1 );
|
||||
cell.replacement.renditions = fb.ds.get_renditions();
|
||||
cell.replacement.contents.clear();
|
||||
cell.replacement.contents.push_back( ch );
|
||||
cell.original_contents.push_back( *fb.get_cell( cursor().row, cursor().col ) );
|
||||
|
||||
/*
|
||||
fprintf( stderr, "[%d=>%d] Predicting %lc in row %d, col %d [tue: %lu]\n",
|
||||
(int)local_frame_acked, (int)cell.expiration_frame,
|
||||
ch, cursor().row, cursor().col,
|
||||
cell.tentative_until_epoch );
|
||||
*/
|
||||
|
||||
cursor().expire( local_frame_sent + 1 );
|
||||
|
||||
/* do we need to wrap? */
|
||||
if ( cursor().col < fb.ds.get_width() - 1 ) {
|
||||
cursor().col++;
|
||||
} else {
|
||||
become_tentative();
|
||||
newline_carriage_return( fb );
|
||||
}
|
||||
}
|
||||
} else if ( typeid( *act ) == typeid( Parser::Execute ) ) {
|
||||
if ( act->char_present && (act->ch == 0x0d) /* CR */ ) {
|
||||
become_tentative();
|
||||
newline_carriage_return( fb );
|
||||
} else {
|
||||
// fprintf( stderr, "Execute 0x%x\n", act->ch );
|
||||
become_tentative();
|
||||
}
|
||||
} else if ( typeid( *act ) == typeid( Parser::Esc_Dispatch ) ) {
|
||||
// fprintf( stderr, "Escape sequence\n" );
|
||||
become_tentative();
|
||||
} else if ( typeid( *act ) == typeid( Parser::CSI_Dispatch ) ) {
|
||||
if ( act->char_present && (act->ch == L'C') ) { /* right arrow */
|
||||
init_cursor( fb );
|
||||
if ( cursor().col < fb.ds.get_width() - 1 ) {
|
||||
cursor().col++;
|
||||
cursor().expire( local_frame_sent + 1 );
|
||||
}
|
||||
} else if ( act->char_present && (act->ch == L'D') ) { /* left arrow */
|
||||
init_cursor( fb );
|
||||
|
||||
if ( cursor().col > 0 ) {
|
||||
cursor().col--;
|
||||
cursor().expire( local_frame_sent + 1 );
|
||||
}
|
||||
} else {
|
||||
// fprintf( stderr, "CSI sequence %lc\n", act->ch );
|
||||
become_tentative();
|
||||
}
|
||||
} else if ( typeid( *act ) == typeid( Parser::Clear ) ) {
|
||||
|
||||
}
|
||||
|
||||
delete act;
|
||||
}
|
||||
}
|
||||
|
||||
void PredictionEngine::newline_carriage_return( const Framebuffer &fb )
|
||||
{
|
||||
init_cursor( fb );
|
||||
cursor().col = 0;
|
||||
if ( cursor().row == fb.ds.get_height() - 1 ) {
|
||||
for ( auto i = overlays.begin(); i != overlays.end(); i++ ) {
|
||||
i->row_num--;
|
||||
for ( auto j = i->overlay_cells.begin(); j != i->overlay_cells.end(); j++ ) {
|
||||
if ( j->active ) {
|
||||
j->expire( local_frame_sent + 1 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* make blank prediction for last row */
|
||||
ConditionalOverlayRow &the_row = get_or_make_row( cursor().row, fb.ds.get_width() );
|
||||
for ( auto j = the_row.overlay_cells.begin(); j != the_row.overlay_cells.end(); j++ ) {
|
||||
j->active = true;
|
||||
j->tentative_until_epoch = prediction_epoch;
|
||||
j->expire( local_frame_sent + 1 );
|
||||
j->replacement.contents.clear();
|
||||
}
|
||||
} else {
|
||||
cursor().row++;
|
||||
}
|
||||
}
|
||||
|
||||
void PredictionEngine::become_tentative( void )
|
||||
{
|
||||
prediction_epoch++;
|
||||
|
||||
/*
|
||||
fprintf( stderr, "Now tentative in epoch %lu (confirmed=%lu)\n",
|
||||
prediction_epoch, confirmed_epoch );
|
||||
*/
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
#ifndef TERMINAL_OVERLAY_HPP
|
||||
#define TERMINAL_OVERLAY_HPP
|
||||
|
||||
#include "terminalframebuffer.h"
|
||||
#include "network.h"
|
||||
#include "parser.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace Overlay {
|
||||
using namespace Terminal;
|
||||
using namespace Network;
|
||||
using namespace std;
|
||||
|
||||
enum Validity {
|
||||
Pending,
|
||||
Correct,
|
||||
CorrectNoCredit,
|
||||
IncorrectOrExpired,
|
||||
Inactive
|
||||
};
|
||||
|
||||
class ConditionalOverlay {
|
||||
public:
|
||||
uint64_t expiration_frame;
|
||||
uint64_t expiration_time; /* after frame is hit */
|
||||
int col;
|
||||
bool active; /* represents a prediction at all */
|
||||
uint64_t tentative_until_epoch; /* when to show */
|
||||
|
||||
ConditionalOverlay( uint64_t s_exp, int s_col, uint64_t s_tentative )
|
||||
: expiration_frame( s_exp ), expiration_time( -1 ), col( s_col ),
|
||||
active( false ),
|
||||
tentative_until_epoch( s_tentative )
|
||||
{}
|
||||
|
||||
virtual ~ConditionalOverlay() {}
|
||||
|
||||
bool tentative( uint64_t confirmed_epoch ) const { return tentative_until_epoch > confirmed_epoch; }
|
||||
void reset( void ) { expiration_frame = expiration_time = tentative_until_epoch = -1; active = false; }
|
||||
bool start_clock( uint64_t local_frame_acked, uint64_t now, unsigned int send_interval );
|
||||
void expire( uint64_t s_exp ) { expiration_frame = s_exp; expiration_time = uint64_t(-1); }
|
||||
};
|
||||
|
||||
class ConditionalCursorMove : public ConditionalOverlay {
|
||||
public:
|
||||
int row;
|
||||
|
||||
void apply( Framebuffer &fb, uint64_t confirmed_epoch ) const;
|
||||
|
||||
Validity get_validity( const Framebuffer &fb, uint64_t sent_frame, uint64_t early_ack, uint64_t late_ack, uint64_t now ) const;
|
||||
|
||||
ConditionalCursorMove( uint64_t s_exp, int s_row, int s_col, uint64_t s_tentative )
|
||||
: ConditionalOverlay( s_exp, s_col, s_tentative ), row( s_row )
|
||||
{}
|
||||
};
|
||||
|
||||
class ConditionalOverlayCell : public ConditionalOverlay {
|
||||
public:
|
||||
Cell replacement;
|
||||
bool unknown;
|
||||
|
||||
vector<Cell> original_contents; /* we don't give credit for correct predictions
|
||||
that match the original contents */
|
||||
|
||||
void apply( Framebuffer &fb, uint64_t confirmed_epoch, int row, bool flag ) const;
|
||||
Validity get_validity( const Framebuffer &fb, int row, uint64_t sent_frame, uint64_t early_ack, uint64_t late_ack, uint64_t now ) const;
|
||||
|
||||
ConditionalOverlayCell( uint64_t s_exp, int s_col, uint64_t s_tentative )
|
||||
: ConditionalOverlay( s_exp, s_col, s_tentative ),
|
||||
replacement( 0 ),
|
||||
unknown( false ),
|
||||
original_contents()
|
||||
{}
|
||||
|
||||
void reset( void ) { unknown = false; original_contents.clear(); ConditionalOverlay::reset(); }
|
||||
void reset_with_orig( void ) {
|
||||
if ( (!active) || unknown ) {
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
|
||||
vector<Cell> new_orig( original_contents );
|
||||
new_orig.push_back( replacement );
|
||||
reset();
|
||||
original_contents = new_orig;
|
||||
}
|
||||
};
|
||||
|
||||
class ConditionalOverlayRow {
|
||||
public:
|
||||
int row_num;
|
||||
|
||||
vector<ConditionalOverlayCell> overlay_cells;
|
||||
|
||||
void apply( Framebuffer &fb, uint64_t confirmed_epoch, bool flag ) const;
|
||||
|
||||
ConditionalOverlayRow( int s_row_num ) : row_num( s_row_num ), overlay_cells() {}
|
||||
};
|
||||
|
||||
/* the various overlays */
|
||||
class NotificationEngine {
|
||||
private:
|
||||
uint64_t last_word_from_server;
|
||||
wstring message;
|
||||
uint64_t message_expiration;
|
||||
|
||||
public:
|
||||
bool need_countup( uint64_t ts ) const { return ts - last_word_from_server > 4500; }
|
||||
void adjust_message( void );
|
||||
void apply( Framebuffer &fb ) const;
|
||||
void set_notification_string( const wstring s_message ) { message = s_message; message_expiration = timestamp() + 1000; }
|
||||
const wstring &get_notification_string( void ) const { return message; }
|
||||
void server_heard( uint64_t s_last_word ) { last_word_from_server = s_last_word; }
|
||||
uint64_t get_message_expiration( void ) const { return message_expiration; }
|
||||
|
||||
NotificationEngine();
|
||||
};
|
||||
|
||||
class PredictionEngine {
|
||||
private:
|
||||
char last_byte;
|
||||
Parser::UTF8Parser parser;
|
||||
|
||||
list<ConditionalOverlayRow> overlays;
|
||||
|
||||
list<ConditionalCursorMove> cursors;
|
||||
|
||||
uint64_t local_frame_sent, local_frame_acked, local_frame_late_acked;
|
||||
|
||||
ConditionalOverlayRow & get_or_make_row( int row_num, int num_cols );
|
||||
|
||||
uint64_t prediction_epoch;
|
||||
uint64_t confirmed_epoch;
|
||||
|
||||
void become_tentative( void );
|
||||
|
||||
void newline_carriage_return( const Framebuffer &fb );
|
||||
|
||||
bool flagging;
|
||||
|
||||
ConditionalCursorMove & cursor( void ) { assert( !cursors.empty() ); return cursors.back(); }
|
||||
|
||||
void kill_epoch( uint64_t epoch, const Framebuffer &fb );
|
||||
|
||||
void init_cursor( const Framebuffer &fb );
|
||||
|
||||
uint64_t last_scheduled_timeout;
|
||||
|
||||
unsigned int send_interval;
|
||||
|
||||
public:
|
||||
void apply( Framebuffer &fb ) const;
|
||||
void new_user_byte( char the_byte, const Framebuffer &fb );
|
||||
void cull( const Framebuffer &fb );
|
||||
|
||||
void reset( void );
|
||||
|
||||
bool active( void ) { return timestamp() <= last_scheduled_timeout; }
|
||||
|
||||
void set_local_frame_sent( uint64_t x ) { local_frame_sent = x; }
|
||||
void set_local_frame_acked( uint64_t x ) { local_frame_acked = x; }
|
||||
void set_local_frame_late_acked( uint64_t x ) { local_frame_late_acked = x; }
|
||||
|
||||
void set_send_interval( unsigned int x ) { send_interval = x; }
|
||||
|
||||
PredictionEngine( void ) : last_byte( 0 ), parser(), overlays(), cursors(),
|
||||
local_frame_sent( 0 ), local_frame_acked( 0 ),
|
||||
local_frame_late_acked( 0 ),
|
||||
prediction_epoch( 1 ), confirmed_epoch( 0 ),
|
||||
flagging( false ), last_scheduled_timeout( 0 ),
|
||||
send_interval( 250 )
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
class TitleEngine {
|
||||
private:
|
||||
deque<wchar_t> prefix;
|
||||
|
||||
public:
|
||||
void apply( Framebuffer &fb ) const { fb.prefix_window_title( prefix ); }
|
||||
void set_prefix( const wstring s );
|
||||
TitleEngine() : prefix() {}
|
||||
};
|
||||
|
||||
/* the overlay manager */
|
||||
class OverlayManager {
|
||||
private:
|
||||
NotificationEngine notifications;
|
||||
PredictionEngine predictions;
|
||||
TitleEngine title;
|
||||
|
||||
public:
|
||||
void apply( Framebuffer &fb );
|
||||
|
||||
NotificationEngine & get_notification_engine( void ) { return notifications; }
|
||||
PredictionEngine & get_prediction_engine( void ) { return predictions; }
|
||||
|
||||
void set_title_prefix( const wstring s ) { title.set_prefix( s ); }
|
||||
|
||||
OverlayManager() : notifications(), predictions(), title() {}
|
||||
|
||||
int wait_time( void );
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user