Convert build system to automake per Anders Kaseorg Makefile.am
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
AM_CPPFLAGS = -I$(top_builddir)/protobufs/ -D_XOPEN_SOURCE=500 -D_GNU_SOURCE -D_BSD_SOURCE
|
||||
AM_CXXFLAGS = -g -O2 --std=c++0x -pedantic -Werror -Wall -Wextra -Weffc++ -fno-default-inline -pipe
|
||||
LIBS = `pkg-config --libs protobuf-lite`
|
||||
LDADD = -lutil -lcrypto -lrt -lm $(top_builddir)/protobufs/libmoshprotos.a
|
||||
|
||||
bin_PROGRAMS = mosh-client mosh-server
|
||||
|
||||
source = parser.cpp parserstate.cpp terminal.cpp parseraction.cpp terminalfunctions.cpp swrite.cpp terminalframebuffer.cpp terminaldispatcher.cpp terminaluserinput.cpp terminaldisplay.cpp network.cpp ocb.cpp base64.cpp crypto.cpp networktransport.cpp transportfragment.cpp user.cpp completeterminal.cpp transportsender.cpp stmclient.cpp terminaloverlay.cpp
|
||||
|
||||
mosh_client_SOURCES = mosh-client.cpp $(source)
|
||||
|
||||
mosh_server_SOURCES = mosh-server.cpp $(source)
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
/* ---------------------------------------------------------------------------
|
||||
*
|
||||
* AEAD API 0.12 - 13 July 2011
|
||||
*
|
||||
* This file gives an interface appropriate for many authenticated
|
||||
* encryption with associated data (AEAD) implementations. It does not try
|
||||
* to accommodate all possible options or limitations that an implementation
|
||||
* might have -- you should consult the documentation of your chosen
|
||||
* implementation to find things like RFC 5116 constants, alignment
|
||||
* requirements, whether the incremental interface is supported, etc.
|
||||
*
|
||||
* This file is in the public domain. It is provided "as is", without
|
||||
* warranty of any kind. Use at your own risk.
|
||||
*
|
||||
* Comments are welcome: Ted Krovetz <ted@krovetz>.
|
||||
*
|
||||
* ------------------------------------------------------------------------ */
|
||||
|
||||
#ifndef _AE_H_
|
||||
#define _AE_H_
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
*
|
||||
* Constants
|
||||
*
|
||||
* ----------------------------------------------------------------------- */
|
||||
|
||||
/* Return status codes: Negative return values indicate an error occurred.
|
||||
* For full explanations of error values, consult the implementation's
|
||||
* documentation. */
|
||||
#define AE_SUCCESS ( 0) /* Indicates successful completion of call */
|
||||
#define AE_INVALID (-1) /* Indicates bad tag during decryption */
|
||||
#define AE_NOT_SUPPORTED (-2) /* Indicates unsupported option requested */
|
||||
|
||||
/* Flags: When data can be processed "incrementally", these flags are used
|
||||
* to indicate whether the submitted data is the last or not. */
|
||||
#define AE_FINALIZE (1) /* This is the last of data */
|
||||
#define AE_PENDING (0) /* More data of is coming */
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
*
|
||||
* AEAD opaque structure definition
|
||||
*
|
||||
* ----------------------------------------------------------------------- */
|
||||
|
||||
typedef struct _ae_ctx ae_ctx;
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
*
|
||||
* Data Structure Routines
|
||||
*
|
||||
* ----------------------------------------------------------------------- */
|
||||
|
||||
ae_ctx* ae_allocate (void *misc); /* Allocate ae_ctx, set optional ptr */
|
||||
void ae_free (ae_ctx *ctx); /* Deallocate ae_ctx struct */
|
||||
int ae_clear (ae_ctx *ctx); /* Undo initialization */
|
||||
int ae_ctx_sizeof(void); /* Return sizeof(ae_ctx) */
|
||||
/* ae_allocate() allocates an ae_ctx structure, but does not initialize it.
|
||||
* ae_free() deallocates an ae_ctx structure, but does not zeroize it.
|
||||
* ae_clear() zeroes sensitive values associated with an ae_ctx structure
|
||||
* and deallocates any auxiliary structures allocated during ae_init().
|
||||
* ae_ctx_sizeof() returns sizeof(ae_ctx), to aid in any static allocations.
|
||||
*/
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
*
|
||||
* AEAD Routines
|
||||
*
|
||||
* ----------------------------------------------------------------------- */
|
||||
|
||||
int ae_init(ae_ctx *ctx,
|
||||
const void *key,
|
||||
int key_len,
|
||||
int nonce_len,
|
||||
int tag_len);
|
||||
/* --------------------------------------------------------------------------
|
||||
*
|
||||
* Initialize an ae_ctx context structure.
|
||||
*
|
||||
* Parameters:
|
||||
* ctx - Pointer to an ae_ctx structure to be initialized
|
||||
* key - Pointer to user-supplied key
|
||||
* key_len - Length of key supplied, in bytes
|
||||
* nonce_len - Length of nonces to be used for this key, in bytes
|
||||
* tag_len - Length of tags to be produced for this key, in bytes
|
||||
*
|
||||
* Returns:
|
||||
* AE_SUCCESS - Success. Ctx ready for use.
|
||||
* AE_NOT_SUPPORTED - An unsupported length was supplied. Ctx is untouched.
|
||||
* Otherwise - Error. Check implementation documentation for codes.
|
||||
*
|
||||
* ----------------------------------------------------------------------- */
|
||||
|
||||
int ae_encrypt(ae_ctx *ctx,
|
||||
const void *nonce,
|
||||
const void *pt,
|
||||
int pt_len,
|
||||
const void *ad,
|
||||
int ad_len,
|
||||
void *ct,
|
||||
void *tag,
|
||||
int final);
|
||||
/* --------------------------------------------------------------------------
|
||||
*
|
||||
* Encrypt plaintext; provide for authentication of ciphertext/associated data.
|
||||
*
|
||||
* Parameters:
|
||||
* ctx - Pointer to an ae_ctx structure initialized by ae_init.
|
||||
* nonce - Pointer to a nonce_len (defined in ae_init) byte nonce.
|
||||
* pt - Pointer to plaintext bytes to be encrypted.
|
||||
* pt_len - number of bytes pointed to by pt.
|
||||
* ad - Pointer to associated data.
|
||||
* ad_len - number of bytes pointed to by ad.
|
||||
* ct - Pointer to buffer to receive ciphertext encryption.
|
||||
* tag - Pointer to receive authentication tag; or NULL
|
||||
* if tag is to be bundled into the ciphertext.
|
||||
* final - Non-zero if this call completes the plaintext being encrypted.
|
||||
*
|
||||
* If nonce!=NULL then a message is being initiated. If final!=0
|
||||
* then a message is being finalized. If final==0 or nonce==NULL
|
||||
* then the incremental interface is being used. If nonce!=NULL and
|
||||
* ad_len<0, then use same ad as last message.
|
||||
*
|
||||
* Returns:
|
||||
* non-negative - Number of bytes written to ct.
|
||||
* AE_NOT_SUPPORTED - Usage mode unsupported (eg, incremental and/or sticky).
|
||||
* Otherwise - Error. Check implementation documentation for codes.
|
||||
*
|
||||
* ----------------------------------------------------------------------- */
|
||||
|
||||
int ae_decrypt(ae_ctx *ctx,
|
||||
const void *nonce,
|
||||
const void *ct,
|
||||
int ct_len,
|
||||
const void *ad,
|
||||
int ad_len,
|
||||
void *pt,
|
||||
const void *tag,
|
||||
int final);
|
||||
/* --------------------------------------------------------------------------
|
||||
*
|
||||
* Decrypt ciphertext; provide authenticity of plaintext and associated data.
|
||||
*
|
||||
* Parameters:
|
||||
* ctx - Pointer to an ae_ctx structure initialized by ae_init.
|
||||
* nonce - Pointer to a nonce_len (defined in ae_init) byte nonce.
|
||||
* ct - Pointer to ciphertext bytes to be decrypted.
|
||||
* ct_len - number of bytes pointed to by ct.
|
||||
* ad - Pointer to associated data.
|
||||
* ad_len - number of bytes pointed to by ad.
|
||||
* pt - Pointer to buffer to receive plaintext decryption.
|
||||
* tag - Pointer to tag_len (defined in ae_init) bytes; or NULL
|
||||
* if tag is bundled into the ciphertext. Non-NULL tag is only
|
||||
* read when final is non-zero.
|
||||
* final - Non-zero if this call completes the ciphertext being decrypted.
|
||||
*
|
||||
* If nonce!=NULL then "ct" points to the start of a ciphertext. If final!=0
|
||||
* then "in" points to the final piece of ciphertext. If final==0 or nonce==
|
||||
* NULL then the incremental interface is being used. If nonce!=NULL and
|
||||
* ad_len<0, then use same ad as last message.
|
||||
*
|
||||
* Returns:
|
||||
* non-negative - Number of bytes written to pt.
|
||||
* AE_INVALID - Authentication failure.
|
||||
* AE_NOT_SUPPORTED - Usage mode unsupported (eg, incremental and/or sticky).
|
||||
* Otherwise - Error. Check implementation documentation for codes.
|
||||
*
|
||||
* NOTE !!! NOTE !!! -- The ciphertext should be assumed possibly inauthentic
|
||||
* until it has been completely written and it is
|
||||
* verified that this routine did not return AE_INVALID.
|
||||
*
|
||||
* ----------------------------------------------------------------------- */
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* closing brace for extern "C" */
|
||||
#endif
|
||||
|
||||
#endif /* _AE_H_ */
|
||||
+577
@@ -0,0 +1,577 @@
|
||||
/* Taken from GNU coreutils */
|
||||
|
||||
/* base64.c -- Encode binary data using printable characters.
|
||||
Copyright (C) 1999-2001, 2004-2006, 2009-2011 Free Software Foundation, Inc.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 3, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */
|
||||
|
||||
/* Written by Simon Josefsson. Partially adapted from GNU MailUtils
|
||||
* (mailbox/filter_trans.c, as of 2004-11-28). Improved by review
|
||||
* from Paul Eggert, Bruno Haible, and Stepan Kasal.
|
||||
*
|
||||
* See also RFC 3548 <http://www.ietf.org/rfc/rfc3548.txt>.
|
||||
*
|
||||
* Be careful with error checking. Here is how you would typically
|
||||
* use these functions:
|
||||
*
|
||||
* bool ok = base64_decode_alloc (in, inlen, &out, &outlen);
|
||||
* if (!ok)
|
||||
* FAIL: input was not valid base64
|
||||
* if (out == NULL)
|
||||
* FAIL: memory allocation error
|
||||
* OK: data in OUT/OUTLEN
|
||||
*
|
||||
* size_t outlen = base64_encode_alloc (in, inlen, &out);
|
||||
* if (out == NULL && outlen == 0 && inlen != 0)
|
||||
* FAIL: input too long
|
||||
* if (out == NULL)
|
||||
* FAIL: memory allocation error
|
||||
* OK: data in OUT/OUTLEN.
|
||||
*
|
||||
*/
|
||||
|
||||
// #include <config.h>
|
||||
|
||||
/* Get prototype. */
|
||||
#include "base64.h"
|
||||
|
||||
/* Get malloc. */
|
||||
#include <stdlib.h>
|
||||
|
||||
/* Get UCHAR_MAX. */
|
||||
#include <limits.h>
|
||||
|
||||
#include <string.h>
|
||||
|
||||
/* C89 compliant way to cast 'char' to 'unsigned char'. */
|
||||
static inline unsigned char
|
||||
to_uchar (char ch)
|
||||
{
|
||||
return ch;
|
||||
}
|
||||
|
||||
/* Base64 encode IN array of size INLEN into OUT array of size OUTLEN.
|
||||
If OUTLEN is less than BASE64_LENGTH(INLEN), write as many bytes as
|
||||
possible. If OUTLEN is larger than BASE64_LENGTH(INLEN), also zero
|
||||
terminate the output buffer. */
|
||||
void
|
||||
base64_encode (const char *restrict in, size_t inlen,
|
||||
char *restrict out, size_t outlen)
|
||||
{
|
||||
static const char b64str[65] = /* KJW */
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
while (inlen && outlen)
|
||||
{
|
||||
*out++ = b64str[(to_uchar (in[0]) >> 2) & 0x3f];
|
||||
if (!--outlen)
|
||||
break;
|
||||
*out++ = b64str[((to_uchar (in[0]) << 4)
|
||||
+ (--inlen ? to_uchar (in[1]) >> 4 : 0))
|
||||
& 0x3f];
|
||||
if (!--outlen)
|
||||
break;
|
||||
*out++ =
|
||||
(inlen
|
||||
? b64str[((to_uchar (in[1]) << 2)
|
||||
+ (--inlen ? to_uchar (in[2]) >> 6 : 0))
|
||||
& 0x3f]
|
||||
: '=');
|
||||
if (!--outlen)
|
||||
break;
|
||||
*out++ = inlen ? b64str[to_uchar (in[2]) & 0x3f] : '=';
|
||||
if (!--outlen)
|
||||
break;
|
||||
if (inlen)
|
||||
inlen--;
|
||||
if (inlen)
|
||||
in += 3;
|
||||
}
|
||||
|
||||
if (outlen)
|
||||
*out = '\0';
|
||||
}
|
||||
|
||||
/* Allocate a buffer and store zero terminated base64 encoded data
|
||||
from array IN of size INLEN, returning BASE64_LENGTH(INLEN), i.e.,
|
||||
the length of the encoded data, excluding the terminating zero. On
|
||||
return, the OUT variable will hold a pointer to newly allocated
|
||||
memory that must be deallocated by the caller. If output string
|
||||
length would overflow, 0 is returned and OUT is set to NULL. If
|
||||
memory allocation failed, OUT is set to NULL, and the return value
|
||||
indicates length of the requested memory block, i.e.,
|
||||
BASE64_LENGTH(inlen) + 1. */
|
||||
size_t
|
||||
base64_encode_alloc (const char *in, size_t inlen, char **out)
|
||||
{
|
||||
size_t outlen = 1 + BASE64_LENGTH (inlen);
|
||||
|
||||
/* Check for overflow in outlen computation.
|
||||
*
|
||||
* If there is no overflow, outlen >= inlen.
|
||||
*
|
||||
* If the operation (inlen + 2) overflows then it yields at most +1, so
|
||||
* outlen is 0.
|
||||
*
|
||||
* If the multiplication overflows, we lose at least half of the
|
||||
* correct value, so the result is < ((inlen + 2) / 3) * 2, which is
|
||||
* less than (inlen + 2) * 0.66667, which is less than inlen as soon as
|
||||
* (inlen > 4).
|
||||
*/
|
||||
if (inlen > outlen)
|
||||
{
|
||||
*out = NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
*out = (char *) malloc (outlen); /* KJW */
|
||||
if (!*out)
|
||||
return outlen;
|
||||
|
||||
base64_encode (in, inlen, *out, outlen);
|
||||
|
||||
return outlen - 1;
|
||||
}
|
||||
|
||||
/* With this approach this file works independent of the charset used
|
||||
(think EBCDIC). However, it does assume that the characters in the
|
||||
Base64 alphabet (A-Za-z0-9+/) are encoded in 0..255. POSIX
|
||||
1003.1-2001 require that char and unsigned char are 8-bit
|
||||
quantities, though, taking care of that problem. But this may be a
|
||||
potential problem on non-POSIX C99 platforms.
|
||||
|
||||
IBM C V6 for AIX mishandles "#define B64(x) ...'x'...", so use "_"
|
||||
as the formal parameter rather than "x". */
|
||||
#define B64(_) \
|
||||
((_) == 'A' ? 0 \
|
||||
: (_) == 'B' ? 1 \
|
||||
: (_) == 'C' ? 2 \
|
||||
: (_) == 'D' ? 3 \
|
||||
: (_) == 'E' ? 4 \
|
||||
: (_) == 'F' ? 5 \
|
||||
: (_) == 'G' ? 6 \
|
||||
: (_) == 'H' ? 7 \
|
||||
: (_) == 'I' ? 8 \
|
||||
: (_) == 'J' ? 9 \
|
||||
: (_) == 'K' ? 10 \
|
||||
: (_) == 'L' ? 11 \
|
||||
: (_) == 'M' ? 12 \
|
||||
: (_) == 'N' ? 13 \
|
||||
: (_) == 'O' ? 14 \
|
||||
: (_) == 'P' ? 15 \
|
||||
: (_) == 'Q' ? 16 \
|
||||
: (_) == 'R' ? 17 \
|
||||
: (_) == 'S' ? 18 \
|
||||
: (_) == 'T' ? 19 \
|
||||
: (_) == 'U' ? 20 \
|
||||
: (_) == 'V' ? 21 \
|
||||
: (_) == 'W' ? 22 \
|
||||
: (_) == 'X' ? 23 \
|
||||
: (_) == 'Y' ? 24 \
|
||||
: (_) == 'Z' ? 25 \
|
||||
: (_) == 'a' ? 26 \
|
||||
: (_) == 'b' ? 27 \
|
||||
: (_) == 'c' ? 28 \
|
||||
: (_) == 'd' ? 29 \
|
||||
: (_) == 'e' ? 30 \
|
||||
: (_) == 'f' ? 31 \
|
||||
: (_) == 'g' ? 32 \
|
||||
: (_) == 'h' ? 33 \
|
||||
: (_) == 'i' ? 34 \
|
||||
: (_) == 'j' ? 35 \
|
||||
: (_) == 'k' ? 36 \
|
||||
: (_) == 'l' ? 37 \
|
||||
: (_) == 'm' ? 38 \
|
||||
: (_) == 'n' ? 39 \
|
||||
: (_) == 'o' ? 40 \
|
||||
: (_) == 'p' ? 41 \
|
||||
: (_) == 'q' ? 42 \
|
||||
: (_) == 'r' ? 43 \
|
||||
: (_) == 's' ? 44 \
|
||||
: (_) == 't' ? 45 \
|
||||
: (_) == 'u' ? 46 \
|
||||
: (_) == 'v' ? 47 \
|
||||
: (_) == 'w' ? 48 \
|
||||
: (_) == 'x' ? 49 \
|
||||
: (_) == 'y' ? 50 \
|
||||
: (_) == 'z' ? 51 \
|
||||
: (_) == '0' ? 52 \
|
||||
: (_) == '1' ? 53 \
|
||||
: (_) == '2' ? 54 \
|
||||
: (_) == '3' ? 55 \
|
||||
: (_) == '4' ? 56 \
|
||||
: (_) == '5' ? 57 \
|
||||
: (_) == '6' ? 58 \
|
||||
: (_) == '7' ? 59 \
|
||||
: (_) == '8' ? 60 \
|
||||
: (_) == '9' ? 61 \
|
||||
: (_) == '+' ? 62 \
|
||||
: (_) == '/' ? 63 \
|
||||
: -1)
|
||||
|
||||
static const signed char b64[0x100] = {
|
||||
B64 (0), B64 (1), B64 (2), B64 (3),
|
||||
B64 (4), B64 (5), B64 (6), B64 (7),
|
||||
B64 (8), B64 (9), B64 (10), B64 (11),
|
||||
B64 (12), B64 (13), B64 (14), B64 (15),
|
||||
B64 (16), B64 (17), B64 (18), B64 (19),
|
||||
B64 (20), B64 (21), B64 (22), B64 (23),
|
||||
B64 (24), B64 (25), B64 (26), B64 (27),
|
||||
B64 (28), B64 (29), B64 (30), B64 (31),
|
||||
B64 (32), B64 (33), B64 (34), B64 (35),
|
||||
B64 (36), B64 (37), B64 (38), B64 (39),
|
||||
B64 (40), B64 (41), B64 (42), B64 (43),
|
||||
B64 (44), B64 (45), B64 (46), B64 (47),
|
||||
B64 (48), B64 (49), B64 (50), B64 (51),
|
||||
B64 (52), B64 (53), B64 (54), B64 (55),
|
||||
B64 (56), B64 (57), B64 (58), B64 (59),
|
||||
B64 (60), B64 (61), B64 (62), B64 (63),
|
||||
B64 (64), B64 (65), B64 (66), B64 (67),
|
||||
B64 (68), B64 (69), B64 (70), B64 (71),
|
||||
B64 (72), B64 (73), B64 (74), B64 (75),
|
||||
B64 (76), B64 (77), B64 (78), B64 (79),
|
||||
B64 (80), B64 (81), B64 (82), B64 (83),
|
||||
B64 (84), B64 (85), B64 (86), B64 (87),
|
||||
B64 (88), B64 (89), B64 (90), B64 (91),
|
||||
B64 (92), B64 (93), B64 (94), B64 (95),
|
||||
B64 (96), B64 (97), B64 (98), B64 (99),
|
||||
B64 (100), B64 (101), B64 (102), B64 (103),
|
||||
B64 (104), B64 (105), B64 (106), B64 (107),
|
||||
B64 (108), B64 (109), B64 (110), B64 (111),
|
||||
B64 (112), B64 (113), B64 (114), B64 (115),
|
||||
B64 (116), B64 (117), B64 (118), B64 (119),
|
||||
B64 (120), B64 (121), B64 (122), B64 (123),
|
||||
B64 (124), B64 (125), B64 (126), B64 (127),
|
||||
B64 (128), B64 (129), B64 (130), B64 (131),
|
||||
B64 (132), B64 (133), B64 (134), B64 (135),
|
||||
B64 (136), B64 (137), B64 (138), B64 (139),
|
||||
B64 (140), B64 (141), B64 (142), B64 (143),
|
||||
B64 (144), B64 (145), B64 (146), B64 (147),
|
||||
B64 (148), B64 (149), B64 (150), B64 (151),
|
||||
B64 (152), B64 (153), B64 (154), B64 (155),
|
||||
B64 (156), B64 (157), B64 (158), B64 (159),
|
||||
B64 (160), B64 (161), B64 (162), B64 (163),
|
||||
B64 (164), B64 (165), B64 (166), B64 (167),
|
||||
B64 (168), B64 (169), B64 (170), B64 (171),
|
||||
B64 (172), B64 (173), B64 (174), B64 (175),
|
||||
B64 (176), B64 (177), B64 (178), B64 (179),
|
||||
B64 (180), B64 (181), B64 (182), B64 (183),
|
||||
B64 (184), B64 (185), B64 (186), B64 (187),
|
||||
B64 (188), B64 (189), B64 (190), B64 (191),
|
||||
B64 (192), B64 (193), B64 (194), B64 (195),
|
||||
B64 (196), B64 (197), B64 (198), B64 (199),
|
||||
B64 (200), B64 (201), B64 (202), B64 (203),
|
||||
B64 (204), B64 (205), B64 (206), B64 (207),
|
||||
B64 (208), B64 (209), B64 (210), B64 (211),
|
||||
B64 (212), B64 (213), B64 (214), B64 (215),
|
||||
B64 (216), B64 (217), B64 (218), B64 (219),
|
||||
B64 (220), B64 (221), B64 (222), B64 (223),
|
||||
B64 (224), B64 (225), B64 (226), B64 (227),
|
||||
B64 (228), B64 (229), B64 (230), B64 (231),
|
||||
B64 (232), B64 (233), B64 (234), B64 (235),
|
||||
B64 (236), B64 (237), B64 (238), B64 (239),
|
||||
B64 (240), B64 (241), B64 (242), B64 (243),
|
||||
B64 (244), B64 (245), B64 (246), B64 (247),
|
||||
B64 (248), B64 (249), B64 (250), B64 (251),
|
||||
B64 (252), B64 (253), B64 (254), B64 (255)
|
||||
};
|
||||
|
||||
#if UCHAR_MAX == 255
|
||||
# define uchar_in_range(c) true
|
||||
#else
|
||||
# define uchar_in_range(c) ((c) <= 255)
|
||||
#endif
|
||||
|
||||
/* Return true if CH is a character from the Base64 alphabet, and
|
||||
false otherwise. Note that '=' is padding and not considered to be
|
||||
part of the alphabet. */
|
||||
bool
|
||||
isbase64 (char ch)
|
||||
{
|
||||
return uchar_in_range (to_uchar (ch)) && 0 <= b64[to_uchar (ch)];
|
||||
}
|
||||
|
||||
/* Initialize decode-context buffer, CTX. */
|
||||
void
|
||||
base64_decode_ctx_init (struct base64_decode_context *ctx)
|
||||
{
|
||||
ctx->i = 0;
|
||||
}
|
||||
|
||||
/* If CTX->i is 0 or 4, there are four or more bytes in [*IN..IN_END), and
|
||||
none of those four is a newline, then return *IN. Otherwise, copy up to
|
||||
4 - CTX->i non-newline bytes from that range into CTX->buf, starting at
|
||||
index CTX->i and setting CTX->i to reflect the number of bytes copied,
|
||||
and return CTX->buf. In either case, advance *IN to point to the byte
|
||||
after the last one processed, and set *N_NON_NEWLINE to the number of
|
||||
verified non-newline bytes accessible through the returned pointer. */
|
||||
static inline char *
|
||||
get_4 (struct base64_decode_context *ctx,
|
||||
char const *restrict *in, char const *restrict in_end,
|
||||
size_t *n_non_newline)
|
||||
{
|
||||
if (ctx->i == 4)
|
||||
ctx->i = 0;
|
||||
|
||||
if (ctx->i == 0)
|
||||
{
|
||||
char const *t = *in;
|
||||
if (4 <= in_end - *in && memchr (t, '\n', 4) == NULL)
|
||||
{
|
||||
/* This is the common case: no newline. */
|
||||
*in += 4;
|
||||
*n_non_newline = 4;
|
||||
return (char *) t;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
/* Copy non-newline bytes into BUF. */
|
||||
char const *p = *in;
|
||||
while (p < in_end)
|
||||
{
|
||||
char c = *p++;
|
||||
if (c != '\n')
|
||||
{
|
||||
ctx->buf[ctx->i++] = c;
|
||||
if (ctx->i == 4)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
*in = p;
|
||||
*n_non_newline = ctx->i;
|
||||
return ctx->buf;
|
||||
}
|
||||
}
|
||||
|
||||
#define return_false \
|
||||
do \
|
||||
{ \
|
||||
*outp = out; \
|
||||
return false; \
|
||||
} \
|
||||
while (false)
|
||||
|
||||
/* Decode up to four bytes of base64-encoded data, IN, of length INLEN
|
||||
into the output buffer, *OUT, of size *OUTLEN bytes. Return true if
|
||||
decoding is successful, false otherwise. If *OUTLEN is too small,
|
||||
as many bytes as possible are written to *OUT. On return, advance
|
||||
*OUT to point to the byte after the last one written, and decrement
|
||||
*OUTLEN to reflect the number of bytes remaining in *OUT. */
|
||||
static inline bool
|
||||
decode_4 (char const *restrict in, size_t inlen,
|
||||
char *restrict *outp, size_t *outleft)
|
||||
{
|
||||
char *out = *outp;
|
||||
if (inlen < 2)
|
||||
return false;
|
||||
|
||||
if (!isbase64 (in[0]) || !isbase64 (in[1]))
|
||||
return false;
|
||||
|
||||
if (*outleft)
|
||||
{
|
||||
*out++ = ((b64[to_uchar (in[0])] << 2)
|
||||
| (b64[to_uchar (in[1])] >> 4));
|
||||
--*outleft;
|
||||
}
|
||||
|
||||
if (inlen == 2)
|
||||
return_false;
|
||||
|
||||
if (in[2] == '=')
|
||||
{
|
||||
if (inlen != 4)
|
||||
return_false;
|
||||
|
||||
if (in[3] != '=')
|
||||
return_false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!isbase64 (in[2]))
|
||||
return_false;
|
||||
|
||||
if (*outleft)
|
||||
{
|
||||
*out++ = (((b64[to_uchar (in[1])] << 4) & 0xf0)
|
||||
| (b64[to_uchar (in[2])] >> 2));
|
||||
--*outleft;
|
||||
}
|
||||
|
||||
if (inlen == 3)
|
||||
return_false;
|
||||
|
||||
if (in[3] == '=')
|
||||
{
|
||||
if (inlen != 4)
|
||||
return_false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!isbase64 (in[3]))
|
||||
return_false;
|
||||
|
||||
if (*outleft)
|
||||
{
|
||||
*out++ = (((b64[to_uchar (in[2])] << 6) & 0xc0)
|
||||
| b64[to_uchar (in[3])]);
|
||||
--*outleft;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*outp = out;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Decode base64-encoded input array IN of length INLEN to output array
|
||||
OUT that can hold *OUTLEN bytes. The input data may be interspersed
|
||||
with newlines. Return true if decoding was successful, i.e. if the
|
||||
input was valid base64 data, false otherwise. If *OUTLEN is too
|
||||
small, as many bytes as possible will be written to OUT. On return,
|
||||
*OUTLEN holds the length of decoded bytes in OUT. Note that as soon
|
||||
as any non-alphabet, non-newline character is encountered, decoding
|
||||
is stopped and false is returned. If INLEN is zero, then process
|
||||
only whatever data is stored in CTX.
|
||||
|
||||
Initially, CTX must have been initialized via base64_decode_ctx_init.
|
||||
Subsequent calls to this function must reuse whatever state is recorded
|
||||
in that buffer. It is necessary for when a quadruple of base64 input
|
||||
bytes spans two input buffers.
|
||||
|
||||
If CTX is NULL then newlines are treated as garbage and the input
|
||||
buffer is processed as a unit. */
|
||||
|
||||
bool
|
||||
base64_decode_ctx (struct base64_decode_context *ctx,
|
||||
const char *restrict in, size_t inlen,
|
||||
char *restrict out, size_t *outlen)
|
||||
{
|
||||
size_t outleft = *outlen;
|
||||
bool ignore_newlines = ctx != NULL;
|
||||
bool flush_ctx = false;
|
||||
unsigned int ctx_i = 0;
|
||||
|
||||
if (ignore_newlines)
|
||||
{
|
||||
ctx_i = ctx->i;
|
||||
flush_ctx = inlen == 0;
|
||||
}
|
||||
|
||||
|
||||
while (true)
|
||||
{
|
||||
size_t outleft_save = outleft;
|
||||
if (ctx_i == 0 && !flush_ctx)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
/* Save a copy of outleft, in case we need to re-parse this
|
||||
block of four bytes. */
|
||||
outleft_save = outleft;
|
||||
if (!decode_4 (in, inlen, &out, &outleft))
|
||||
break;
|
||||
|
||||
in += 4;
|
||||
inlen -= 4;
|
||||
}
|
||||
}
|
||||
|
||||
if (inlen == 0 && !flush_ctx)
|
||||
break;
|
||||
|
||||
/* Handle the common case of 72-byte wrapped lines.
|
||||
This also handles any other multiple-of-4-byte wrapping. */
|
||||
if (inlen && *in == '\n' && ignore_newlines)
|
||||
{
|
||||
++in;
|
||||
--inlen;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Restore OUT and OUTLEFT. */
|
||||
out -= outleft_save - outleft;
|
||||
outleft = outleft_save;
|
||||
|
||||
{
|
||||
char const *in_end = in + inlen;
|
||||
char const *non_nl;
|
||||
|
||||
if (ignore_newlines)
|
||||
non_nl = get_4 (ctx, &in, in_end, &inlen);
|
||||
else
|
||||
non_nl = in; /* Might have nl in this case. */
|
||||
|
||||
/* If the input is empty or consists solely of newlines (0 non-newlines),
|
||||
then we're done. Likewise if there are fewer than 4 bytes when not
|
||||
flushing context and not treating newlines as garbage. */
|
||||
if (inlen == 0 || (inlen < 4 && !flush_ctx && ignore_newlines))
|
||||
{
|
||||
inlen = 0;
|
||||
break;
|
||||
}
|
||||
if (!decode_4 (non_nl, inlen, &out, &outleft))
|
||||
break;
|
||||
|
||||
inlen = in_end - in;
|
||||
}
|
||||
}
|
||||
|
||||
*outlen -= outleft;
|
||||
|
||||
return inlen == 0;
|
||||
}
|
||||
|
||||
/* Allocate an output buffer in *OUT, and decode the base64 encoded
|
||||
data stored in IN of size INLEN to the *OUT buffer. On return, the
|
||||
size of the decoded data is stored in *OUTLEN. OUTLEN may be NULL,
|
||||
if the caller is not interested in the decoded length. *OUT may be
|
||||
NULL to indicate an out of memory error, in which case *OUTLEN
|
||||
contains the size of the memory block needed. The function returns
|
||||
true on successful decoding and memory allocation errors. (Use the
|
||||
*OUT and *OUTLEN parameters to differentiate between successful
|
||||
decoding and memory error.) The function returns false if the
|
||||
input was invalid, in which case *OUT is NULL and *OUTLEN is
|
||||
undefined. */
|
||||
bool
|
||||
base64_decode_alloc_ctx (struct base64_decode_context *ctx,
|
||||
const char *in, size_t inlen, char **out,
|
||||
size_t *outlen)
|
||||
{
|
||||
/* This may allocate a few bytes too many, depending on input,
|
||||
but it's not worth the extra CPU time to compute the exact size.
|
||||
The exact size is 3 * inlen / 4, minus 1 if the input ends
|
||||
with "=" and minus another 1 if the input ends with "==".
|
||||
Dividing before multiplying avoids the possibility of overflow. */
|
||||
size_t needlen = 3 * (inlen / 4) + 2;
|
||||
|
||||
*out = (char *) malloc (needlen);
|
||||
if (!*out)
|
||||
return true;
|
||||
|
||||
if (!base64_decode_ctx (ctx, in, inlen, *out, &needlen))
|
||||
{
|
||||
free (*out);
|
||||
*out = NULL;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (outlen)
|
||||
*outlen = needlen;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/* Taken from GNU coreutils */
|
||||
|
||||
#define restrict
|
||||
|
||||
/* base64.h -- Encode binary data using printable characters.
|
||||
Copyright (C) 2004-2006, 2009-2011 Free Software Foundation, Inc.
|
||||
Written by Simon Josefsson.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 3, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */
|
||||
|
||||
#ifndef BASE64_H
|
||||
# define BASE64_H
|
||||
|
||||
/* Get size_t. */
|
||||
# include <stddef.h>
|
||||
|
||||
/* Get bool. */
|
||||
# include <stdbool.h>
|
||||
|
||||
/* This uses that the expression (n+(k-1))/k means the smallest
|
||||
integer >= n/k, i.e., the ceiling of n/k. */
|
||||
# define BASE64_LENGTH(inlen) ((((inlen) + 2) / 3) * 4)
|
||||
|
||||
struct base64_decode_context
|
||||
{
|
||||
unsigned int i;
|
||||
char buf[4];
|
||||
};
|
||||
|
||||
extern bool isbase64 (char ch);
|
||||
|
||||
extern void base64_encode (const char *restrict in, size_t inlen,
|
||||
char *restrict out, size_t outlen);
|
||||
|
||||
extern size_t base64_encode_alloc (const char *in, size_t inlen, char **out);
|
||||
|
||||
extern void base64_decode_ctx_init (struct base64_decode_context *ctx);
|
||||
|
||||
extern bool base64_decode_ctx (struct base64_decode_context *ctx,
|
||||
const char *restrict in, size_t inlen,
|
||||
char *restrict out, size_t *outlen);
|
||||
|
||||
extern bool base64_decode_alloc_ctx (struct base64_decode_context *ctx,
|
||||
const char *in, size_t inlen,
|
||||
char **out, size_t *outlen);
|
||||
|
||||
#define base64_decode(in, inlen, out, outlen) \
|
||||
base64_decode_ctx (NULL, in, inlen, out, outlen)
|
||||
|
||||
#define base64_decode_alloc(in, inlen, out, outlen) \
|
||||
base64_decode_alloc_ctx (NULL, in, inlen, out, outlen)
|
||||
|
||||
#endif /* BASE64_H */
|
||||
@@ -0,0 +1,75 @@
|
||||
#include "completeterminal.hpp"
|
||||
|
||||
#include "hostinput.pb.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace Parser;
|
||||
using namespace Terminal;
|
||||
using namespace HostBuffers;
|
||||
|
||||
string Complete::act( const string &str )
|
||||
{
|
||||
for ( unsigned int i = 0; i < str.size(); i++ ) {
|
||||
/* parse octet into up to three actions */
|
||||
list<Action *> actions( parser.input( str[ i ] ) );
|
||||
|
||||
/* apply actions to terminal and delete them */
|
||||
for ( list<Action *>::iterator it = actions.begin();
|
||||
it != actions.end();
|
||||
it++ ) {
|
||||
Action *act = *it;
|
||||
act->act_on_terminal( &terminal );
|
||||
delete act;
|
||||
}
|
||||
}
|
||||
|
||||
return terminal.read_octets_to_host();
|
||||
}
|
||||
|
||||
string Complete::act( const Action *act )
|
||||
{
|
||||
/* apply action to terminal */
|
||||
act->act_on_terminal( &terminal );
|
||||
return terminal.read_octets_to_host();
|
||||
}
|
||||
|
||||
/* interface for Network::Transport */
|
||||
string Complete::diff_from( const Complete &existing )
|
||||
{
|
||||
HostBuffers::HostMessage output;
|
||||
|
||||
if ( !(existing.get_fb() == get_fb()) ) {
|
||||
if ( (existing.get_fb().ds.get_width() != terminal.get_fb().ds.get_width())
|
||||
|| (existing.get_fb().ds.get_height() != terminal.get_fb().ds.get_height()) ) {
|
||||
Instruction *new_res = output.add_instruction();
|
||||
new_res->MutableExtension( resize )->set_width( terminal.get_fb().ds.get_width() );
|
||||
new_res->MutableExtension( resize )->set_height( terminal.get_fb().ds.get_height() );
|
||||
}
|
||||
Instruction *new_inst = output.add_instruction();
|
||||
new_inst->MutableExtension( hostbytes )->set_hoststring( Terminal::Display::new_frame( true, existing.get_fb(), terminal.get_fb() ) );
|
||||
}
|
||||
|
||||
return output.SerializeAsString();
|
||||
}
|
||||
|
||||
void Complete::apply_string( string diff )
|
||||
{
|
||||
HostBuffers::HostMessage input;
|
||||
assert( input.ParseFromString( diff ) );
|
||||
|
||||
for ( int i = 0; i < input.instruction_size(); i++ ) {
|
||||
if ( input.instruction( i ).HasExtension( hostbytes ) ) {
|
||||
string terminal_to_host = act( input.instruction( i ).GetExtension( hostbytes ).hoststring() );
|
||||
assert( terminal_to_host.empty() ); /* server never interrogates client terminal */
|
||||
} else if ( input.instruction( i ).HasExtension( resize ) ) {
|
||||
act( new Resize( input.instruction( i ).GetExtension( resize ).width(),
|
||||
input.instruction( i ).GetExtension( resize ).height() ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Complete::operator==( Complete const &x ) const
|
||||
{
|
||||
// assert( parser == x.parser ); /* parser state is irrelevant for us */
|
||||
return terminal == x.terminal;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#ifndef COMPLETE_TERMINAL_HPP
|
||||
#define COMPLETE_TERMINAL_HPP
|
||||
|
||||
#include "parser.hpp"
|
||||
#include "terminal.hpp"
|
||||
|
||||
/* This class represents the complete terminal -- a UTF8Parser feeding Actions to an Emulator. */
|
||||
|
||||
namespace Terminal {
|
||||
class Complete {
|
||||
private:
|
||||
Parser::UTF8Parser parser;
|
||||
Terminal::Emulator terminal;
|
||||
|
||||
public:
|
||||
Complete( size_t width, size_t height ) : parser(), terminal( width, height ) {}
|
||||
|
||||
std::string act( const std::string &str );
|
||||
std::string act( const Parser::Action *act );
|
||||
|
||||
const Framebuffer & get_fb( void ) const { return terminal.get_fb(); }
|
||||
bool parser_grounded( void ) const { return parser.is_grounded(); }
|
||||
|
||||
/* interface for Network::Transport */
|
||||
void subtract( const Complete * ) {}
|
||||
std::string diff_from( const Complete &existing );
|
||||
void apply_string( std::string diff );
|
||||
bool operator==( const Complete &x ) const;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "crypto.hpp"
|
||||
#include "base64.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace Crypto;
|
||||
|
||||
const char rdev[] = "/dev/urandom";
|
||||
|
||||
long int myatoi( char *str )
|
||||
{
|
||||
char *end;
|
||||
|
||||
errno = 0;
|
||||
long int ret = strtol( str, &end, 10 );
|
||||
|
||||
if ( ( errno != 0 )
|
||||
|| ( end != str + strlen( str ) ) ) {
|
||||
throw CryptoException( "Bad integer." );
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void * sse_alloc( int len )
|
||||
{
|
||||
void *ptr = NULL;
|
||||
|
||||
if( (0 != posix_memalign( (void **)&ptr, 16, len )) || (ptr == NULL) ) {
|
||||
throw std::bad_alloc();
|
||||
}
|
||||
|
||||
return ptr;
|
||||
}
|
||||
|
||||
Base64Key::Base64Key( string printable_key )
|
||||
{
|
||||
if ( printable_key.length() != 22 ) {
|
||||
throw CryptoException( "Key must be 22 letters long." );
|
||||
}
|
||||
|
||||
string base64 = printable_key + "==";
|
||||
|
||||
size_t len = 16;
|
||||
if ( !base64_decode( base64.data(), 24, (char *)&key[ 0 ], &len ) ) {
|
||||
throw CryptoException( "Key must be well-formed base64." );
|
||||
}
|
||||
|
||||
if ( len != 16 ) {
|
||||
throw CryptoException( "Key must represent 16 octets." );
|
||||
}
|
||||
|
||||
/* to catch changes after the first 128 bits */
|
||||
if ( printable_key != this->printable_key() ) {
|
||||
throw CryptoException( "Base64 key was not encoded 128-bit key." );
|
||||
}
|
||||
}
|
||||
|
||||
Base64Key::Base64Key()
|
||||
{
|
||||
FILE *devrandom = fopen( rdev, "r" );
|
||||
if ( devrandom == NULL ) {
|
||||
throw CryptoException( string( rdev ) + ": " + strerror( errno ) );
|
||||
}
|
||||
|
||||
if ( 1 != fread( key, 16, 1, devrandom ) ) {
|
||||
throw CryptoException( "Could not read from " + string( rdev ) );
|
||||
}
|
||||
|
||||
if ( 0 != fclose( devrandom ) ) {
|
||||
throw CryptoException( string( rdev ) + ": " + strerror( errno ) );
|
||||
}
|
||||
}
|
||||
|
||||
string Base64Key::printable_key( void ) const
|
||||
{
|
||||
char base64[ 25 ];
|
||||
|
||||
base64_encode( (char *)key, 16, base64, 25 );
|
||||
|
||||
if ( (base64[ 24 ] != 0)
|
||||
|| (base64[ 23 ] != '=')
|
||||
|| (base64[ 22 ] != '=') ) {
|
||||
throw CryptoException( "Unexpected output from base64_encode." );
|
||||
}
|
||||
|
||||
base64[ 22 ] = 0;
|
||||
return string( base64 );
|
||||
}
|
||||
|
||||
Session::Session( Base64Key s_key )
|
||||
: key( s_key ), ctx( NULL )
|
||||
{
|
||||
ctx = ae_allocate( NULL );
|
||||
if ( ctx == NULL ) {
|
||||
throw CryptoException( "Could not allocate AES-OCB context." );
|
||||
}
|
||||
|
||||
if ( AE_SUCCESS != ae_init( ctx, key.data(), 16, 12, 16 ) ) {
|
||||
throw CryptoException( "Could not initialize AES-OCB context." );
|
||||
}
|
||||
}
|
||||
|
||||
Session::~Session()
|
||||
{
|
||||
if ( ae_clear( ctx ) != AE_SUCCESS ) {
|
||||
throw CryptoException( "Could not clear AES-OCB context." );
|
||||
}
|
||||
|
||||
ae_free( ctx );
|
||||
}
|
||||
|
||||
Nonce::Nonce( uint64_t val )
|
||||
{
|
||||
uint64_t val_net = htobe64( val );
|
||||
|
||||
memset( bytes, 0, 4 );
|
||||
memcpy( bytes + 4, &val_net, 8 );
|
||||
}
|
||||
|
||||
uint64_t Nonce::val( void )
|
||||
{
|
||||
uint64_t ret;
|
||||
memcpy( &ret, bytes + 4, 8 );
|
||||
return be64toh( ret );
|
||||
}
|
||||
|
||||
Nonce::Nonce( char *s_bytes, size_t len )
|
||||
{
|
||||
if ( len != 8 ) {
|
||||
throw CryptoException( "Nonce representation must be 8 octets long." );
|
||||
}
|
||||
|
||||
memset( bytes, 0, 4 );
|
||||
memcpy( bytes + 4, s_bytes, 8 );
|
||||
}
|
||||
|
||||
Message::Message( char *nonce_bytes, size_t nonce_len,
|
||||
char *text_bytes, size_t text_len )
|
||||
: nonce( nonce_bytes, nonce_len ),
|
||||
text( (char *)text_bytes, text_len )
|
||||
{}
|
||||
|
||||
Message::Message( Nonce s_nonce, string s_text )
|
||||
: nonce( s_nonce ),
|
||||
text( s_text )
|
||||
{}
|
||||
|
||||
string Session::encrypt( Message plaintext )
|
||||
{
|
||||
const size_t pt_len = plaintext.text.size();
|
||||
const int ciphertext_len = pt_len + 16;
|
||||
|
||||
char *ciphertext = (char *)sse_alloc( ciphertext_len );
|
||||
char *pt = (char *)sse_alloc( pt_len );
|
||||
|
||||
memcpy( pt, plaintext.text.data(), plaintext.text.size() );
|
||||
|
||||
if ( (uint64_t( plaintext.nonce.data() ) & 0xf) != 0 ) {
|
||||
throw CryptoException( "Bad alignment." );
|
||||
}
|
||||
|
||||
if ( ciphertext_len != ae_encrypt( ctx, /* ctx */
|
||||
plaintext.nonce.data(), /* nonce */
|
||||
pt, /* pt */
|
||||
pt_len, /* pt_len */
|
||||
NULL, /* ad */
|
||||
0, /* ad_len */
|
||||
ciphertext, /* ct */
|
||||
NULL, /* tag */
|
||||
AE_FINALIZE ) ) { /* final */
|
||||
free( pt );
|
||||
free( ciphertext );
|
||||
throw CryptoException( "ae_encrypt() returned error." );
|
||||
}
|
||||
|
||||
string text( (char *)ciphertext, ciphertext_len );
|
||||
free( pt );
|
||||
free( ciphertext );
|
||||
|
||||
return plaintext.nonce.cpp_str() + text;
|
||||
}
|
||||
|
||||
Message Session::decrypt( string ciphertext )
|
||||
{
|
||||
if ( ciphertext.size() < 24 ) {
|
||||
throw CryptoException( "Ciphertext must contain nonce and tag." );
|
||||
}
|
||||
|
||||
char *str = (char *)ciphertext.data();
|
||||
|
||||
int body_len = ciphertext.size() - 8;
|
||||
int pt_len = body_len - 16;
|
||||
|
||||
if ( pt_len < 0 ) { /* super-assertion that pt_len does not equal AE_INVALID */
|
||||
fprintf( stderr, "BUG.\n" );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
Nonce __attribute__((__aligned__ (16))) nonce( str, 8 );
|
||||
char *body = (char *)sse_alloc( body_len );
|
||||
memcpy( body, str + 8, body_len );
|
||||
|
||||
char *plaintext = (char *)sse_alloc( pt_len );
|
||||
|
||||
if ( pt_len != ae_decrypt( ctx, /* ctx */
|
||||
nonce.data(), /* nonce */
|
||||
body, /* ct */
|
||||
body_len, /* ct_len */
|
||||
NULL, /* ad */
|
||||
0, /* ad_len */
|
||||
plaintext, /* pt */
|
||||
NULL, /* tag */
|
||||
AE_FINALIZE ) ) { /* final */
|
||||
free( plaintext );
|
||||
free( body );
|
||||
throw CryptoException( "Packet failed integrity check." );
|
||||
}
|
||||
|
||||
Message ret( nonce, string( plaintext, pt_len ) );
|
||||
free( plaintext );
|
||||
free( body );
|
||||
|
||||
return ret;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
#ifndef CRYPTO_HPP
|
||||
#define CRYPTO_HPP
|
||||
|
||||
#include "ae.hpp"
|
||||
#include <string>
|
||||
#include <string.h>
|
||||
|
||||
using namespace std;
|
||||
|
||||
long int myatoi( char *str );
|
||||
|
||||
namespace Crypto {
|
||||
class CryptoException {
|
||||
public:
|
||||
string text;
|
||||
CryptoException( string s_text ) : text( s_text ) {};
|
||||
};
|
||||
|
||||
class Base64Key {
|
||||
private:
|
||||
unsigned char key[ 16 ];
|
||||
|
||||
public:
|
||||
Base64Key(); /* random key */
|
||||
Base64Key( string printable_key );
|
||||
string printable_key( void ) const;
|
||||
unsigned char *data( void ) { return key; }
|
||||
};
|
||||
|
||||
class Nonce {
|
||||
private:
|
||||
char bytes[ 12 ];
|
||||
|
||||
public:
|
||||
Nonce( uint64_t val );
|
||||
Nonce( char *s_bytes, size_t len );
|
||||
|
||||
string cpp_str( void ) { return string( (char *)( bytes + 4 ), 8 ); }
|
||||
char *data( void ) { return bytes; }
|
||||
uint64_t val( void );
|
||||
};
|
||||
|
||||
class Message {
|
||||
public:
|
||||
Nonce nonce;
|
||||
string text;
|
||||
|
||||
Message( char *nonce_bytes, size_t nonce_len,
|
||||
char *text_bytes, size_t text_len );
|
||||
Message( Nonce s_nonce, string s_text );
|
||||
};
|
||||
|
||||
class Session {
|
||||
private:
|
||||
Base64Key key;
|
||||
ae_ctx *ctx;
|
||||
|
||||
public:
|
||||
Session( Base64Key s_key );
|
||||
~Session();
|
||||
|
||||
string encrypt( Message plaintext );
|
||||
Message decrypt( string ciphertext );
|
||||
|
||||
Session( const Session & );
|
||||
Session & operator=( const Session & );
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,22 @@
|
||||
#ifndef DOS_ASSERT_HPP
|
||||
#define DOS_ASSERT_HPP
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "crypto.hpp"
|
||||
|
||||
static void dos_detected( const char *expression, const char *file, int line, const char *function )
|
||||
{
|
||||
char buffer[ 2048 ];
|
||||
snprintf( buffer, 2048, "Illegal counterparty input (possible denial of service) in function %s at %s:%d, failed test: %s\n",
|
||||
function, file, line, expression );
|
||||
throw Crypto::CryptoException( buffer );
|
||||
}
|
||||
|
||||
#define dos_assert(expr) \
|
||||
((expr) \
|
||||
? (void)0 \
|
||||
: dos_detected (__STRING(expr), __FILE__, __LINE__, __PRETTY_FUNCTION__ ))
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,58 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "stmclient.hpp"
|
||||
#include "crypto.hpp"
|
||||
|
||||
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.hpp"
|
||||
#include "swrite.hpp"
|
||||
#include "user.hpp"
|
||||
|
||||
#include "networktransport.cpp"
|
||||
|
||||
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() );
|
||||
}
|
||||
}
|
||||
}
|
||||
+359
@@ -0,0 +1,359 @@
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <assert.h>
|
||||
#include <endian.h>
|
||||
|
||||
#include "dos_assert.hpp"
|
||||
#include "network.hpp"
|
||||
#include "crypto.hpp"
|
||||
|
||||
using namespace std;
|
||||
using namespace Network;
|
||||
using namespace Crypto;
|
||||
|
||||
const uint64_t DIRECTION_MASK = uint64_t(1) << 63;
|
||||
const uint64_t SEQUENCE_MASK = uint64_t(-1) ^ DIRECTION_MASK;
|
||||
|
||||
/* Read in packet from coded string */
|
||||
Packet::Packet( string coded_packet, Session *session )
|
||||
: seq( -1 ),
|
||||
direction( TO_SERVER ),
|
||||
timestamp( -1 ),
|
||||
timestamp_reply( -1 ),
|
||||
payload()
|
||||
{
|
||||
Message message = session->decrypt( coded_packet );
|
||||
|
||||
direction = (message.nonce.val() & DIRECTION_MASK) ? TO_CLIENT : TO_SERVER;
|
||||
seq = message.nonce.val() & SEQUENCE_MASK;
|
||||
|
||||
dos_assert( message.text.size() >= 2 * sizeof( uint16_t ) );
|
||||
|
||||
uint16_t *data = (uint16_t *)message.text.data();
|
||||
timestamp = be16toh( data[ 0 ] );
|
||||
timestamp_reply = be16toh( data[ 1 ] );
|
||||
|
||||
payload = string( message.text.begin() + 2 * sizeof( uint16_t ), message.text.end() );
|
||||
}
|
||||
|
||||
/* Output coded string from packet */
|
||||
string Packet::tostring( Session *session )
|
||||
{
|
||||
uint64_t direction_seq = (uint64_t( direction == TO_CLIENT ) << 63) | (seq & SEQUENCE_MASK);
|
||||
|
||||
uint16_t ts_net[ 2 ] = { htobe16( timestamp ), htobe16( timestamp_reply ) };
|
||||
|
||||
string timestamps = string( (char *)ts_net, 2 * sizeof( uint16_t ) );
|
||||
|
||||
return session->encrypt( Message( Nonce( direction_seq ), timestamps + payload ) );
|
||||
}
|
||||
|
||||
Packet Connection::new_packet( string &s_payload )
|
||||
{
|
||||
uint16_t outgoing_timestamp_reply = -1;
|
||||
|
||||
uint64_t now = timestamp();
|
||||
|
||||
if ( now - saved_timestamp_received_at < 1000 ) { /* we have a recent received timestamp */
|
||||
/* send "corrected" timestamp advanced by how long we held it */
|
||||
outgoing_timestamp_reply = saved_timestamp + (now - saved_timestamp_received_at);
|
||||
saved_timestamp = -1;
|
||||
saved_timestamp_received_at = 0;
|
||||
}
|
||||
|
||||
Packet p( next_seq++, direction, timestamp16(), outgoing_timestamp_reply, s_payload );
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
void Connection::setup( void )
|
||||
{
|
||||
/* create socket */
|
||||
sock = socket( AF_INET, SOCK_DGRAM, 0 );
|
||||
if ( sock < 0 ) {
|
||||
throw NetworkException( "socket", errno );
|
||||
}
|
||||
|
||||
/* Enable path MTU discovery */
|
||||
char flag = IP_PMTUDISC_DO;
|
||||
socklen_t optlen = sizeof( flag );
|
||||
if ( setsockopt( sock, IPPROTO_IP, IP_MTU_DISCOVER, &flag, optlen ) < 0 ) {
|
||||
throw NetworkException( "setsockopt", errno );
|
||||
}
|
||||
}
|
||||
|
||||
Connection::Connection( const char *desired_ip ) /* server */
|
||||
: sock( -1 ),
|
||||
remote_addr(),
|
||||
server( true ),
|
||||
attached( false ),
|
||||
MTU( SEND_MTU ),
|
||||
key(),
|
||||
session( key ),
|
||||
direction( TO_CLIENT ),
|
||||
next_seq( 0 ),
|
||||
saved_timestamp( -1 ),
|
||||
saved_timestamp_received_at( 0 ),
|
||||
expected_receiver_seq( 0 ),
|
||||
RTT_hit( false ),
|
||||
SRTT( 1000 ),
|
||||
RTTVAR( 500 )
|
||||
{
|
||||
setup();
|
||||
|
||||
/* Attempt to bind free local port, with
|
||||
address client used to connect to us.
|
||||
|
||||
This usage does not seem to be endorsed by POSIX. */
|
||||
|
||||
struct sockaddr_in local_addr;
|
||||
local_addr.sin_family = AF_INET;
|
||||
local_addr.sin_port = htons( 0 );
|
||||
if ( desired_ip
|
||||
&& inet_aton( desired_ip, &local_addr.sin_addr )
|
||||
&& (bind( sock, (sockaddr *)&local_addr, sizeof( local_addr ) ) == 0) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( desired_ip ) {
|
||||
fprintf( stderr, "Could not bind to desired local address %s.\n", desired_ip );
|
||||
}
|
||||
|
||||
/* Could not bind to that IP (maybe we are behind NAT).
|
||||
Try again with any IP. */
|
||||
local_addr.sin_addr.s_addr = INADDR_ANY;
|
||||
if ( bind( sock, (sockaddr *)&local_addr, sizeof( local_addr ) ) < 0 ) {
|
||||
throw NetworkException( "bind", errno );
|
||||
}
|
||||
}
|
||||
|
||||
Connection::Connection( const char *key_str, const char *ip, int port ) /* client */
|
||||
: sock( -1 ),
|
||||
remote_addr(),
|
||||
server( false ),
|
||||
attached( false ),
|
||||
MTU( SEND_MTU ),
|
||||
key( key_str ),
|
||||
session( key ),
|
||||
direction( TO_SERVER ),
|
||||
next_seq( 0 ),
|
||||
saved_timestamp( -1 ),
|
||||
saved_timestamp_received_at( 0 ),
|
||||
expected_receiver_seq( 0 ),
|
||||
RTT_hit( false ),
|
||||
SRTT( 1000 ),
|
||||
RTTVAR( 500 )
|
||||
{
|
||||
setup();
|
||||
|
||||
/* associate socket with remote host and port */
|
||||
remote_addr.sin_family = AF_INET;
|
||||
remote_addr.sin_port = htons( port );
|
||||
if ( !inet_aton( ip, &remote_addr.sin_addr ) ) {
|
||||
int saved_errno = errno;
|
||||
char buffer[ 2048 ];
|
||||
snprintf( buffer, 2048, "Bad IP address (%s)", ip );
|
||||
throw NetworkException( buffer, saved_errno );
|
||||
}
|
||||
|
||||
attached = true;
|
||||
}
|
||||
|
||||
void Connection::send( string s )
|
||||
{
|
||||
assert( attached );
|
||||
|
||||
Packet px = new_packet( s );
|
||||
|
||||
string p = px.tostring( &session );
|
||||
|
||||
ssize_t bytes_sent = sendto( sock, p.data(), p.size(), 0,
|
||||
(sockaddr *)&remote_addr, sizeof( remote_addr ) );
|
||||
|
||||
if ( (bytes_sent < 0) && (errno == EMSGSIZE) ) {
|
||||
update_MTU();
|
||||
throw NetworkException( "Path MTU Discovery", EMSGSIZE );
|
||||
} else if ( bytes_sent == static_cast<int>( p.size() ) ) {
|
||||
return;
|
||||
} else {
|
||||
throw NetworkException( "sendto", errno );
|
||||
}
|
||||
}
|
||||
|
||||
string Connection::recv( void )
|
||||
{
|
||||
struct sockaddr_in packet_remote_addr;
|
||||
|
||||
char buf[ RECEIVE_MTU ];
|
||||
|
||||
socklen_t addrlen = sizeof( packet_remote_addr );
|
||||
|
||||
ssize_t received_len = recvfrom( sock, buf, RECEIVE_MTU, 0, (sockaddr *)&packet_remote_addr, &addrlen );
|
||||
|
||||
if ( received_len < 0 ) {
|
||||
throw NetworkException( "recvfrom", errno );
|
||||
}
|
||||
|
||||
if ( received_len > RECEIVE_MTU ) {
|
||||
char buffer[ 2048 ];
|
||||
snprintf( buffer, 2048, "Received oversize datagram (size %d) and limit is %d\n",
|
||||
static_cast<int>( received_len ), RECEIVE_MTU );
|
||||
throw NetworkException( buffer, errno );
|
||||
}
|
||||
|
||||
Packet p( string( buf, received_len ), &session );
|
||||
|
||||
dos_assert( p.direction == (server ? TO_SERVER : TO_CLIENT) ); /* prevent malicious playback to sender */
|
||||
|
||||
if ( p.seq >= expected_receiver_seq ) { /* don't use out-of-order packets for timestamp or targeting */
|
||||
expected_receiver_seq = p.seq + 1; /* this is security-sensitive because a replay attack could otherwise
|
||||
screw up the timestamp and targeting */
|
||||
|
||||
if ( p.timestamp != uint16_t(-1) ) {
|
||||
saved_timestamp = p.timestamp;
|
||||
saved_timestamp_received_at = timestamp();
|
||||
}
|
||||
|
||||
if ( p.timestamp_reply != uint16_t(-1) ) {
|
||||
uint16_t now = timestamp16();
|
||||
double R = timestamp_diff( now, p.timestamp_reply );
|
||||
|
||||
if ( R < 5000 ) { /* ignore large values, e.g. server was Ctrl-Zed */
|
||||
if ( !RTT_hit ) { /* first measurement */
|
||||
SRTT = R;
|
||||
RTTVAR = R / 2;
|
||||
RTT_hit = true;
|
||||
} else {
|
||||
const double alpha = 1.0 / 8.0;
|
||||
const double beta = 1.0 / 4.0;
|
||||
|
||||
RTTVAR = (1 - beta) * RTTVAR + ( beta * fabs( SRTT - R ) );
|
||||
SRTT = (1 - alpha) * SRTT + ( alpha * R );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* auto-adjust to remote host */
|
||||
attached = true;
|
||||
|
||||
if ( (remote_addr.sin_addr.s_addr != packet_remote_addr.sin_addr.s_addr)
|
||||
|| (remote_addr.sin_port != packet_remote_addr.sin_port) ) {
|
||||
remote_addr = packet_remote_addr;
|
||||
if ( server ) {
|
||||
fprintf( stderr, "Server now attached to client at %s:%d\n",
|
||||
inet_ntoa( remote_addr.sin_addr ),
|
||||
ntohs( remote_addr.sin_port ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return p.payload; /* we do return out-of-order or duplicated packets to caller */
|
||||
}
|
||||
|
||||
int Connection::port( void ) const
|
||||
{
|
||||
struct sockaddr_in local_addr;
|
||||
socklen_t addrlen = sizeof( local_addr );
|
||||
|
||||
if ( getsockname( sock, (sockaddr *)&local_addr, &addrlen ) < 0 ) {
|
||||
throw NetworkException( "getsockname", errno );
|
||||
}
|
||||
|
||||
return ntohs( local_addr.sin_port );
|
||||
}
|
||||
|
||||
uint64_t Network::timestamp( void )
|
||||
{
|
||||
struct timespec tp;
|
||||
|
||||
if ( clock_gettime( CLOCK_MONOTONIC, &tp ) < 0 ) {
|
||||
throw NetworkException( "clock_gettime", errno );
|
||||
}
|
||||
|
||||
uint64_t millis = tp.tv_nsec / 1000000;
|
||||
millis += uint64_t( tp.tv_sec ) * 1000;
|
||||
|
||||
return millis;
|
||||
}
|
||||
|
||||
uint16_t Network::timestamp16( void )
|
||||
{
|
||||
uint16_t ts = timestamp() % 65536;
|
||||
if ( ts == uint16_t(-1) ) {
|
||||
ts++;
|
||||
}
|
||||
return ts;
|
||||
}
|
||||
|
||||
uint16_t Network::timestamp_diff( uint16_t tsnew, uint16_t tsold )
|
||||
{
|
||||
int diff = tsnew - tsold;
|
||||
if ( diff < 0 ) {
|
||||
diff += 65536;
|
||||
}
|
||||
|
||||
assert( diff >= 0 );
|
||||
assert( diff <= 65535 );
|
||||
|
||||
return diff;
|
||||
}
|
||||
|
||||
uint64_t Connection::timeout( void ) const
|
||||
{
|
||||
uint64_t RTO = lrint( ceil( SRTT + 4 * RTTVAR ) );
|
||||
if ( RTO < MIN_RTO ) {
|
||||
RTO = MIN_RTO;
|
||||
} else if ( RTO > MAX_RTO ) {
|
||||
RTO = MAX_RTO;
|
||||
}
|
||||
return RTO;
|
||||
}
|
||||
|
||||
class Socket {
|
||||
public:
|
||||
int fd;
|
||||
|
||||
Socket( int domain, int type, int protocol )
|
||||
: fd( socket( domain, type, protocol ) )
|
||||
{
|
||||
if ( fd < 0 ) {
|
||||
throw NetworkException( "socket", errno );
|
||||
}
|
||||
}
|
||||
|
||||
~Socket()
|
||||
{
|
||||
if ( close( fd ) < 0 ) {
|
||||
throw NetworkException( "close", errno );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void Connection::update_MTU( void )
|
||||
{
|
||||
if ( !attached ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* We don't want to use our main socket because we don't want to have to connect it */
|
||||
Socket path_MTU_socket( AF_INET, SOCK_DGRAM, 0 );
|
||||
|
||||
/* Connect socket so we can retrieve path MTU */
|
||||
if ( connect( path_MTU_socket.fd, (sockaddr *)&remote_addr, sizeof( remote_addr ) ) < 0 ) {
|
||||
throw NetworkException( "connect", errno );
|
||||
}
|
||||
|
||||
int PMTU;
|
||||
socklen_t optlen = sizeof( PMTU );
|
||||
if ( getsockopt( path_MTU_socket.fd, IPPROTO_IP, IP_MTU, &PMTU, &optlen ) < 0 ) {
|
||||
throw NetworkException( "getsockopt", errno );
|
||||
}
|
||||
|
||||
if ( optlen != sizeof( PMTU ) ) {
|
||||
throw NetworkException( "Error getting path MTU", errno );
|
||||
}
|
||||
|
||||
MTU = min( PMTU, int(SEND_MTU) ); /* need cast to compile without optimization! XXX */
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
#ifndef NETWORK_HPP
|
||||
#define NETWORK_HPP
|
||||
|
||||
#include <stdint.h>
|
||||
#include <deque>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <string>
|
||||
#include <math.h>
|
||||
|
||||
#include "crypto.hpp"
|
||||
|
||||
using namespace std;
|
||||
using namespace Crypto;
|
||||
|
||||
namespace Network {
|
||||
static const unsigned int MOSH_PROTOCOL_VERSION = 1;
|
||||
|
||||
uint64_t timestamp( void );
|
||||
uint16_t timestamp16( void );
|
||||
uint16_t timestamp_diff( uint16_t tsnew, uint16_t tsold );
|
||||
|
||||
class NetworkException {
|
||||
public:
|
||||
string function;
|
||||
int the_errno;
|
||||
NetworkException( string s_function, int s_errno ) : function( s_function ), the_errno( s_errno ) {}
|
||||
};
|
||||
|
||||
enum Direction {
|
||||
TO_SERVER = 0,
|
||||
TO_CLIENT = 1
|
||||
};
|
||||
|
||||
class Packet {
|
||||
public:
|
||||
uint64_t seq;
|
||||
Direction direction;
|
||||
uint16_t timestamp, timestamp_reply;
|
||||
string payload;
|
||||
|
||||
Packet( uint64_t s_seq, Direction s_direction,
|
||||
uint16_t s_timestamp, uint16_t s_timestamp_reply, string s_payload )
|
||||
: seq( s_seq ), direction( s_direction ),
|
||||
timestamp( s_timestamp ), timestamp_reply( s_timestamp_reply ), payload( s_payload )
|
||||
{}
|
||||
|
||||
Packet( string coded_packet, Session *session );
|
||||
|
||||
string tostring( Session *session );
|
||||
};
|
||||
|
||||
class Connection {
|
||||
private:
|
||||
static const int RECEIVE_MTU = 2048;
|
||||
static const int SEND_MTU = 1400;
|
||||
static const uint64_t MIN_RTO = 50; /* ms */
|
||||
static const uint64_t MAX_RTO = 1000; /* ms */
|
||||
|
||||
int sock;
|
||||
struct sockaddr_in remote_addr;
|
||||
|
||||
bool server;
|
||||
bool attached;
|
||||
|
||||
int MTU;
|
||||
|
||||
Base64Key key;
|
||||
Session session;
|
||||
|
||||
void setup( void );
|
||||
|
||||
Direction direction;
|
||||
uint64_t next_seq;
|
||||
uint16_t saved_timestamp;
|
||||
uint64_t saved_timestamp_received_at;
|
||||
uint64_t expected_receiver_seq;
|
||||
|
||||
bool RTT_hit;
|
||||
double SRTT;
|
||||
double RTTVAR;
|
||||
|
||||
Packet new_packet( string &s_payload );
|
||||
|
||||
void update_MTU( void );
|
||||
|
||||
public:
|
||||
Connection( const char *desired_ip ); /* server */
|
||||
Connection( const char *key_str, const char *ip, int port ); /* client */
|
||||
|
||||
void send( string s );
|
||||
string recv( void );
|
||||
int fd( void ) const { return sock; }
|
||||
int get_MTU( void ) const { return MTU; }
|
||||
|
||||
int port( void ) const;
|
||||
string get_key( void ) const { return key.printable_key(); }
|
||||
bool get_attached( void ) const { return attached; }
|
||||
|
||||
uint64_t timeout( void ) const;
|
||||
double get_SRTT( void ) const { return SRTT; }
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,154 @@
|
||||
#include <assert.h>
|
||||
#include <iostream>
|
||||
|
||||
#include "networktransport.hpp"
|
||||
|
||||
#include "transportsender.cpp"
|
||||
|
||||
using namespace Network;
|
||||
using namespace std;
|
||||
|
||||
template <class MyState, class RemoteState>
|
||||
Transport<MyState, RemoteState>::Transport( MyState &initial_state, RemoteState &initial_remote,
|
||||
const char *desired_ip )
|
||||
: connection( desired_ip ),
|
||||
sender( &connection, initial_state ),
|
||||
received_states( 1, TimestampedState<RemoteState>( timestamp(), 0, initial_remote ) ),
|
||||
last_receiver_state( initial_remote ),
|
||||
sent_state_late_acked( 0 ),
|
||||
fragments(),
|
||||
verbose( false )
|
||||
{
|
||||
/* server */
|
||||
}
|
||||
|
||||
template <class MyState, class RemoteState>
|
||||
Transport<MyState, RemoteState>::Transport( MyState &initial_state, RemoteState &initial_remote,
|
||||
const char *key_str, const char *ip, int port )
|
||||
: connection( key_str, ip, port ),
|
||||
sender( &connection, initial_state ),
|
||||
received_states( 1, TimestampedState<RemoteState>( timestamp(), 0, initial_remote ) ),
|
||||
last_receiver_state( initial_remote ),
|
||||
sent_state_late_acked( 0 ),
|
||||
fragments(),
|
||||
verbose( false )
|
||||
{
|
||||
/* client */
|
||||
}
|
||||
|
||||
template <class MyState, class RemoteState>
|
||||
void Transport<MyState, RemoteState>::recv( void )
|
||||
{
|
||||
string s( connection.recv() );
|
||||
Fragment frag( s );
|
||||
|
||||
if ( fragments.add_fragment( frag ) ) { /* complete packet */
|
||||
Instruction inst = fragments.get_assembly();
|
||||
|
||||
if ( inst.protocol_version() != MOSH_PROTOCOL_VERSION ) {
|
||||
throw NetworkException( "mosh protocol version mismatch", 0 );
|
||||
}
|
||||
|
||||
sender.process_acknowledgment_through( inst.ack_num() );
|
||||
if ( inst.late_ack_num() > sent_state_late_acked ) {
|
||||
sent_state_late_acked = inst.late_ack_num();
|
||||
}
|
||||
|
||||
/* first, make sure we don't already have the new state */
|
||||
for ( typename list< TimestampedState<RemoteState> >::iterator i = received_states.begin();
|
||||
i != received_states.end();
|
||||
i++ ) {
|
||||
if ( inst.new_num() == i->num ) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* now, make sure we do have the old state */
|
||||
bool found = 0;
|
||||
typename list< TimestampedState<RemoteState> >::iterator reference_state = received_states.begin();
|
||||
while ( reference_state != received_states.end() ) {
|
||||
if ( inst.old_num() == reference_state->num ) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
reference_state++;
|
||||
}
|
||||
|
||||
if ( !found ) {
|
||||
// fprintf( stderr, "Ignoring out-of-order packet. Reference state %d has been discarded or hasn't yet been received.\n", int(inst.old_num) );
|
||||
return; /* this is security-sensitive and part of how we enforce idempotency */
|
||||
}
|
||||
|
||||
/* apply diff to reference state */
|
||||
TimestampedState<RemoteState> new_state = *reference_state;
|
||||
new_state.timestamp = timestamp();
|
||||
new_state.num = inst.new_num();
|
||||
|
||||
if ( !inst.diff().empty() ) {
|
||||
new_state.state.apply_string( inst.diff() );
|
||||
}
|
||||
|
||||
process_throwaway_until( inst.throwaway_num() );
|
||||
|
||||
/* Insert new state in sorted place */
|
||||
for ( typename list< TimestampedState<RemoteState> >::iterator i = received_states.begin();
|
||||
i != received_states.end();
|
||||
i++ ) {
|
||||
if ( i->num > new_state.num ) {
|
||||
received_states.insert( i, new_state );
|
||||
if ( verbose ) {
|
||||
fprintf( stderr, "[%u] Received OUT-OF-ORDER state %d [ack %d]\n",
|
||||
(unsigned int)(timestamp() % 100000), (int)new_state.num, (int)inst.ack_num() );
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if ( verbose ) {
|
||||
fprintf( stderr, "[%u] Received state %d [ack %d]\n",
|
||||
(unsigned int)(timestamp() % 100000), (int)new_state.num, (int)inst.ack_num() );
|
||||
}
|
||||
received_states.push_back( new_state );
|
||||
sender.set_ack_num( received_states.back().num );
|
||||
|
||||
if ( !inst.diff().empty() ) {
|
||||
sender.set_data_ack();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* The sender uses throwaway_num to tell us the earliest received state that we need to keep around */
|
||||
template <class MyState, class RemoteState>
|
||||
void Transport<MyState, RemoteState>::process_throwaway_until( uint64_t throwaway_num )
|
||||
{
|
||||
typename list< TimestampedState<RemoteState> >::iterator i = received_states.begin();
|
||||
while ( i != received_states.end() ) {
|
||||
typename list< TimestampedState<RemoteState> >::iterator inext = i;
|
||||
inext++;
|
||||
if ( i->num < throwaway_num ) {
|
||||
received_states.erase( i );
|
||||
}
|
||||
i = inext;
|
||||
}
|
||||
|
||||
assert( received_states.size() > 0 );
|
||||
}
|
||||
|
||||
template <class MyState, class RemoteState>
|
||||
string Transport<MyState, RemoteState>::get_remote_diff( void )
|
||||
{
|
||||
/* find diff between last receiver state and current remote state, then rationalize states */
|
||||
|
||||
string ret( received_states.back().state.diff_from( last_receiver_state ) );
|
||||
|
||||
const RemoteState *oldest_receiver_state = &received_states.front().state;
|
||||
|
||||
for ( typename list< TimestampedState<RemoteState> >::reverse_iterator i = received_states.rbegin();
|
||||
i != received_states.rend();
|
||||
i++ ) {
|
||||
i->state.subtract( oldest_receiver_state );
|
||||
}
|
||||
|
||||
last_receiver_state = received_states.back().state;
|
||||
|
||||
return ret;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
#ifndef NETWORK_TRANSPORT_HPP
|
||||
#define NETWORK_TRANSPORT_HPP
|
||||
|
||||
#include <string>
|
||||
#include <signal.h>
|
||||
#include <time.h>
|
||||
#include <list>
|
||||
#include <vector>
|
||||
|
||||
#include "network.hpp"
|
||||
#include "transportsender.hpp"
|
||||
#include "transportfragment.hpp"
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace Network {
|
||||
template <class MyState, class RemoteState>
|
||||
class Transport
|
||||
{
|
||||
private:
|
||||
/* the underlying, encrypted network connection */
|
||||
Connection connection;
|
||||
|
||||
/* sender side */
|
||||
TransportSender<MyState> sender;
|
||||
|
||||
/* helper methods for recv() */
|
||||
void process_throwaway_until( uint64_t throwaway_num );
|
||||
|
||||
/* simple receiver */
|
||||
list< TimestampedState<RemoteState> > received_states;
|
||||
RemoteState last_receiver_state; /* the state we were in when user last queried state */
|
||||
uint64_t sent_state_late_acked;
|
||||
FragmentAssembly fragments;
|
||||
bool verbose;
|
||||
|
||||
public:
|
||||
Transport( MyState &initial_state, RemoteState &initial_remote, const char *desired_ip );
|
||||
Transport( MyState &initial_state, RemoteState &initial_remote,
|
||||
const char *key_str, const char *ip, int port );
|
||||
|
||||
/* Send data or an ack if necessary. */
|
||||
void tick( void ) { sender.tick(); }
|
||||
|
||||
/* Returns the number of ms to wait until next possible event. */
|
||||
int wait_time( void ) { return sender.wait_time(); }
|
||||
|
||||
/* Blocks waiting for a packet. */
|
||||
void recv( void );
|
||||
|
||||
/* Find diff between last receiver state and current remote state, then rationalize states. */
|
||||
string get_remote_diff( void );
|
||||
|
||||
/* Shut down other side of connection. */
|
||||
/* Illegal to change current_state after this. */
|
||||
void start_shutdown( void ) { sender.start_shutdown(); }
|
||||
bool shutdown_in_progress( void ) const { return sender.get_shutdown_in_progress(); }
|
||||
bool shutdown_acknowledged( void ) const { return sender.get_shutdown_acknowledged(); }
|
||||
bool shutdown_ack_timed_out( void ) const { return sender.shutdown_ack_timed_out(); }
|
||||
bool attached( void ) const { return connection.get_attached(); }
|
||||
|
||||
/* Other side has requested shutdown and we have sent one ACK */
|
||||
bool counterparty_shutdown_ack_sent( void ) const { return sender.get_counterparty_shutdown_acknowledged(); }
|
||||
|
||||
int port( void ) const { return connection.port(); }
|
||||
string get_key( void ) const { return connection.get_key(); }
|
||||
|
||||
MyState &get_current_state( void ) { return sender.get_current_state(); }
|
||||
void set_current_state( const MyState &x ) { sender.set_current_state( x ); }
|
||||
|
||||
uint64_t get_remote_state_num( void ) const { return received_states.back().num; }
|
||||
|
||||
const TimestampedState<RemoteState> & get_latest_remote_state( void ) const { return received_states.back(); }
|
||||
|
||||
int fd( void ) const { return connection.fd(); }
|
||||
|
||||
void set_verbose( void ) { sender.set_verbose(); verbose = true; }
|
||||
|
||||
void set_send_delay( int new_delay ) { sender.set_send_delay( new_delay ); }
|
||||
|
||||
uint64_t get_sent_state_acked( void ) const { return sender.get_sent_state_acked(); }
|
||||
uint64_t get_sent_state_last( void ) const { return sender.get_sent_state_last(); }
|
||||
uint64_t get_sent_state_late_acked( void ) const { return sent_state_late_acked; }
|
||||
|
||||
unsigned int send_interval( void ) const { return sender.send_interval(); }
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
+1226
File diff suppressed because it is too large
Load Diff
+127
@@ -0,0 +1,127 @@
|
||||
#include <assert.h>
|
||||
#include <typeinfo>
|
||||
#include <langinfo.h>
|
||||
|
||||
#include "parser.hpp"
|
||||
|
||||
static void append_or_delete( Parser::Action *act,
|
||||
std::list<Parser::Action *>&vec )
|
||||
{
|
||||
assert( act );
|
||||
|
||||
if ( typeid( *act ) != typeid( Parser::Ignore ) ) {
|
||||
vec.push_back( act );
|
||||
} else {
|
||||
delete act;
|
||||
}
|
||||
}
|
||||
|
||||
std::list<Parser::Action *> Parser::Parser::input( wchar_t ch )
|
||||
{
|
||||
std::list<Action *> ret;
|
||||
|
||||
Transition tx = state->input( ch );
|
||||
|
||||
if ( tx.next_state != NULL ) {
|
||||
append_or_delete( state->exit(), ret );
|
||||
}
|
||||
|
||||
append_or_delete( tx.action, ret );
|
||||
|
||||
if ( tx.next_state != NULL ) {
|
||||
append_or_delete( tx.next_state->enter(), ret );
|
||||
state = tx.next_state;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
Parser::UTF8Parser::UTF8Parser()
|
||||
: parser(), buf_len( 0 )
|
||||
{
|
||||
assert( BUF_SIZE >= MB_CUR_MAX );
|
||||
}
|
||||
|
||||
std::list<Parser::Action *> Parser::UTF8Parser::input( char c )
|
||||
{
|
||||
assert( buf_len < BUF_SIZE );
|
||||
|
||||
buf[ buf_len++ ] = c;
|
||||
|
||||
/* This function will only work in a UTF-8 locale. */
|
||||
/* This is asserted in the constructor. */
|
||||
|
||||
wchar_t pwc;
|
||||
mbstate_t ps;
|
||||
memset( &ps, 0, sizeof( ps ) );
|
||||
|
||||
size_t total_bytes_parsed = 0;
|
||||
size_t orig_buf_len = buf_len;
|
||||
std::list<Action *> ret;
|
||||
|
||||
/* this routine is somewhat complicated in order to comply with
|
||||
Unicode 6.0, section 3.9, "Best Practices for using U+FFFD" */
|
||||
|
||||
while ( total_bytes_parsed != orig_buf_len ) {
|
||||
assert( total_bytes_parsed < orig_buf_len );
|
||||
assert( buf_len > 0 );
|
||||
size_t bytes_parsed = mbrtowc( &pwc, buf, buf_len, &ps );
|
||||
|
||||
/* this returns 0 when n = 0! */
|
||||
|
||||
/* This function annoying returns a size_t so we have to check
|
||||
the negative values first before the "> 0" branch */
|
||||
|
||||
if ( bytes_parsed == 0 ) {
|
||||
/* character was NUL, accept and clear buffer */
|
||||
assert( buf_len == 1 );
|
||||
buf_len = 0;
|
||||
pwc = L'\0';
|
||||
bytes_parsed = 1;
|
||||
} else if ( bytes_parsed == (size_t) -1 ) {
|
||||
/* invalid sequence, use replacement character and try again with last char */
|
||||
assert( errno == EILSEQ );
|
||||
if ( buf_len > 1 ) {
|
||||
buf[ 0 ] = buf[ buf_len - 1 ];
|
||||
bytes_parsed = buf_len - 1;
|
||||
buf_len = 1;
|
||||
} else {
|
||||
buf_len = 0;
|
||||
bytes_parsed = 1;
|
||||
}
|
||||
pwc = (wchar_t) 0xFFFD;
|
||||
} else if ( bytes_parsed == (size_t) -2 ) {
|
||||
/* can't parse incomplete multibyte character */
|
||||
total_bytes_parsed += buf_len;
|
||||
continue;
|
||||
} else if ( bytes_parsed > 0 ) {
|
||||
/* parsed into pwc, accept */
|
||||
assert( bytes_parsed <= buf_len );
|
||||
memcpy( buf, buf + bytes_parsed, buf_len - bytes_parsed );
|
||||
buf_len = buf_len - bytes_parsed;
|
||||
} else {
|
||||
throw std::string( "Unknown return value from mbrtowc" );
|
||||
}
|
||||
|
||||
if ( (pwc < 0) || (pwc > 0x10FFFF) ) { /* outside Unicode range */
|
||||
pwc = (wchar_t) 0xFFFD;
|
||||
}
|
||||
|
||||
std::list<Action *> vec = parser.input( pwc );
|
||||
ret.insert( ret.end(), vec.begin(), vec.end() );
|
||||
|
||||
total_bytes_parsed += bytes_parsed;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
Parser::Parser::Parser( const Parser &other )
|
||||
: state( other.state )
|
||||
{}
|
||||
|
||||
Parser::Parser & Parser::Parser::operator=( const Parser &other )
|
||||
{
|
||||
state = other.state;
|
||||
return *this;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
#ifndef PARSER_HPP
|
||||
#define PARSER_HPP
|
||||
|
||||
/* Based on Paul Williams's parser,
|
||||
http://www.vt100.net/emu/dec_ansi_parser */
|
||||
|
||||
#include <wchar.h>
|
||||
#include <list>
|
||||
#include <string.h>
|
||||
|
||||
#include "parsertransition.hpp"
|
||||
#include "parseraction.hpp"
|
||||
#include "parserstate.hpp"
|
||||
#include "parserstatefamily.hpp"
|
||||
|
||||
#ifndef __STDC_ISO_10646__
|
||||
#error "Must have __STDC_ISO_10646__"
|
||||
#endif
|
||||
|
||||
namespace Parser {
|
||||
static const StateFamily family;
|
||||
|
||||
class Parser {
|
||||
private:
|
||||
State const *state;
|
||||
|
||||
public:
|
||||
Parser() : state( &family.s_Ground ) {}
|
||||
|
||||
Parser( const Parser &other );
|
||||
Parser & operator=( const Parser & );
|
||||
~Parser() {}
|
||||
|
||||
std::list<Action *> input( wchar_t ch );
|
||||
|
||||
bool operator==( const Parser &x ) const
|
||||
{
|
||||
return state == x.state;
|
||||
}
|
||||
|
||||
bool is_grounded( void ) const { return state == &family.s_Ground; }
|
||||
};
|
||||
|
||||
static const size_t BUF_SIZE = 8;
|
||||
|
||||
class UTF8Parser {
|
||||
private:
|
||||
Parser parser;
|
||||
|
||||
char buf[ BUF_SIZE ];
|
||||
size_t buf_len;
|
||||
|
||||
public:
|
||||
UTF8Parser();
|
||||
|
||||
std::list<Action *> input( char c );
|
||||
|
||||
bool operator==( const UTF8Parser &x ) const
|
||||
{
|
||||
return parser == x.parser;
|
||||
}
|
||||
|
||||
bool is_grounded( void ) const { return parser.is_grounded(); }
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,86 @@
|
||||
#include <stdio.h>
|
||||
#include <wctype.h>
|
||||
|
||||
#include "parseraction.hpp"
|
||||
#include "terminal.hpp"
|
||||
|
||||
using namespace Parser;
|
||||
|
||||
std::string Action::str( void )
|
||||
{
|
||||
char thechar[ 10 ] = { 0 };
|
||||
if ( char_present ) {
|
||||
snprintf( thechar, 10, iswprint( ch ) ? "(%lc)" : "(0x%x)", ch );
|
||||
}
|
||||
|
||||
return name() + std::string( thechar );
|
||||
}
|
||||
|
||||
void Print::act_on_terminal( Terminal::Emulator *emu ) const
|
||||
{
|
||||
emu->print( this );
|
||||
}
|
||||
|
||||
void Execute::act_on_terminal( Terminal::Emulator *emu ) const
|
||||
{
|
||||
emu->execute( this );
|
||||
}
|
||||
|
||||
void Clear::act_on_terminal( Terminal::Emulator *emu ) const
|
||||
{
|
||||
emu->dispatch.clear( this );
|
||||
}
|
||||
|
||||
void Param::act_on_terminal( Terminal::Emulator *emu ) const
|
||||
{
|
||||
emu->dispatch.newparamchar( this );
|
||||
}
|
||||
|
||||
void Collect::act_on_terminal( Terminal::Emulator *emu ) const
|
||||
{
|
||||
emu->dispatch.collect( this );
|
||||
}
|
||||
|
||||
void CSI_Dispatch::act_on_terminal( Terminal::Emulator *emu ) const
|
||||
{
|
||||
emu->CSI_dispatch( this );
|
||||
}
|
||||
|
||||
void Esc_Dispatch::act_on_terminal( Terminal::Emulator *emu ) const
|
||||
{
|
||||
emu->Esc_dispatch( this );
|
||||
}
|
||||
|
||||
void OSC_Put::act_on_terminal( Terminal::Emulator *emu ) const
|
||||
{
|
||||
emu->dispatch.OSC_put( this );
|
||||
}
|
||||
|
||||
void OSC_Start::act_on_terminal( Terminal::Emulator *emu ) const
|
||||
{
|
||||
emu->dispatch.OSC_start( this );
|
||||
}
|
||||
|
||||
void OSC_End::act_on_terminal( Terminal::Emulator *emu ) const
|
||||
{
|
||||
emu->OSC_end( this );
|
||||
}
|
||||
|
||||
void UserByte::act_on_terminal( Terminal::Emulator *emu ) const
|
||||
{
|
||||
emu->dispatch.terminal_to_host.append( emu->user.input( this,
|
||||
emu->fb.ds.application_mode_cursor_keys ) );
|
||||
}
|
||||
|
||||
void Resize::act_on_terminal( Terminal::Emulator *emu ) const
|
||||
{
|
||||
emu->resize( width, height );
|
||||
handled = true;
|
||||
}
|
||||
|
||||
bool Action::operator==( const Action &other ) const
|
||||
{
|
||||
return ( char_present == other.char_present )
|
||||
&& ( ch == other.ch )
|
||||
&& ( handled == other.handled );
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
#ifndef PARSERACTION_HPP
|
||||
#define PARSERACTION_HPP
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace Terminal {
|
||||
class Emulator;
|
||||
}
|
||||
|
||||
namespace Parser {
|
||||
class Action
|
||||
{
|
||||
public:
|
||||
bool char_present;
|
||||
wchar_t ch;
|
||||
mutable bool handled;
|
||||
|
||||
std::string str( void );
|
||||
|
||||
virtual std::string name( void ) = 0;
|
||||
|
||||
virtual void act_on_terminal( Terminal::Emulator * ) const {};
|
||||
|
||||
Action() : char_present( false ), ch( -1 ), handled( false ) {};
|
||||
virtual ~Action() {};
|
||||
|
||||
virtual bool operator==( const Action &other ) const;
|
||||
};
|
||||
|
||||
class Ignore : public Action {
|
||||
public: std::string name( void ) { return std::string( "Ignore" ); }
|
||||
};
|
||||
class Print : public Action {
|
||||
public:
|
||||
std::string name( void ) { return std::string( "Print" ); }
|
||||
void act_on_terminal( Terminal::Emulator *emu ) const;
|
||||
};
|
||||
class Execute : public Action {
|
||||
public:
|
||||
std::string name( void ) { return std::string( "Execute" ); }
|
||||
void act_on_terminal( Terminal::Emulator *emu ) const;
|
||||
};
|
||||
class Clear : public Action {
|
||||
public:
|
||||
std::string name( void ) { return std::string( "Clear" ); }
|
||||
void act_on_terminal( Terminal::Emulator *emu ) const;
|
||||
};
|
||||
class Collect : public Action {
|
||||
public:
|
||||
std::string name( void ) { return std::string( "Collect" ); }
|
||||
void act_on_terminal( Terminal::Emulator *emu ) const;
|
||||
};
|
||||
class Param : public Action {
|
||||
public:
|
||||
std::string name( void ) { return std::string( "Param" ); }
|
||||
void act_on_terminal( Terminal::Emulator *emu ) const;
|
||||
};
|
||||
class Esc_Dispatch : public Action {
|
||||
public:
|
||||
std::string name( void ) { return std::string( "Esc_Dispatch" ); }
|
||||
void act_on_terminal( Terminal::Emulator *emu ) const;
|
||||
};
|
||||
class CSI_Dispatch : public Action {
|
||||
public:
|
||||
std::string name( void ) { return std::string( "CSI_Dispatch" ); }
|
||||
void act_on_terminal( Terminal::Emulator *emu ) const;
|
||||
};
|
||||
class Hook : public Action {
|
||||
public: std::string name( void ) { return std::string( "Hook" ); }
|
||||
};
|
||||
class Put : public Action {
|
||||
public: std::string name( void ) { return std::string( "Put" ); }
|
||||
};
|
||||
class Unhook : public Action {
|
||||
public: std::string name( void ) { return std::string( "Unhook" ); }
|
||||
};
|
||||
class OSC_Start : public Action {
|
||||
public:
|
||||
std::string name( void ) { return std::string( "OSC_Start" ); }
|
||||
void act_on_terminal( Terminal::Emulator *emu ) const;
|
||||
};
|
||||
class OSC_Put : public Action {
|
||||
public:
|
||||
std::string name( void ) { return std::string( "OSC_Put" ); }
|
||||
void act_on_terminal( Terminal::Emulator *emu ) const;
|
||||
};
|
||||
class OSC_End : public Action {
|
||||
public:
|
||||
std::string name( void ) { return std::string( "OSC_End" ); }
|
||||
void act_on_terminal( Terminal::Emulator *emu ) const;
|
||||
};
|
||||
|
||||
class UserByte : public Action {
|
||||
/* user keystroke -- not part of the host-source state machine*/
|
||||
public:
|
||||
char c; /* The user-source byte. We don't try to interpret the charset */
|
||||
|
||||
std::string name( void ) { return std::string( "UserByte" ); }
|
||||
void act_on_terminal( Terminal::Emulator *emu ) const;
|
||||
|
||||
UserByte( int s_c ) : c( s_c ) {}
|
||||
|
||||
bool operator==( const UserByte &other ) const
|
||||
{
|
||||
return c == other.c;
|
||||
}
|
||||
};
|
||||
|
||||
class Resize : public Action {
|
||||
/* resize event -- not part of the host-source state machine*/
|
||||
public:
|
||||
size_t width, height;
|
||||
|
||||
std::string name( void ) { return std::string( "Resize" ); }
|
||||
void act_on_terminal( Terminal::Emulator *emu ) const;
|
||||
|
||||
Resize( size_t s_width, size_t s_height )
|
||||
: width( s_width ),
|
||||
height( s_height )
|
||||
{}
|
||||
|
||||
bool operator==( const Resize &other ) const
|
||||
{
|
||||
return ( width == other.width ) && ( height == other.height );
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,356 @@
|
||||
#include "parserstate.hpp"
|
||||
#include "parserstatefamily.hpp"
|
||||
|
||||
using namespace Parser;
|
||||
|
||||
Transition State::anywhere_rule( wchar_t ch ) const
|
||||
{
|
||||
if ( (ch == 0x18) || (ch == 0x1A)
|
||||
|| ((0x80 <= ch) && (ch <= 0x8F))
|
||||
|| ((0x91 <= ch) && (ch <= 0x97))
|
||||
|| (ch == 0x99) || (ch == 0x9A) ) {
|
||||
return Transition( new Execute, &family->s_Ground );
|
||||
} else if ( ch == 0x9C ) {
|
||||
return Transition( &family->s_Ground );
|
||||
} else if ( ch == 0x1B ) {
|
||||
return Transition( &family->s_Escape );
|
||||
} else if ( (ch == 0x98) || (ch == 0x9E) || (ch == 0x9F) ) {
|
||||
return Transition( &family->s_SOS_PM_APC_String );
|
||||
} else if ( ch == 0x90 ) {
|
||||
return Transition( &family->s_DCS_Entry );
|
||||
} else if ( ch == 0x9D ) {
|
||||
return Transition( &family->s_OSC_String );
|
||||
} else if ( ch == 0x9B ) {
|
||||
return Transition( &family->s_CSI_Entry );
|
||||
}
|
||||
|
||||
return Transition( NULL, NULL ); /* don't allocate an Ignore action */
|
||||
}
|
||||
|
||||
Transition State::input( wchar_t ch ) const
|
||||
{
|
||||
Transition ret = anywhere_rule( ch );
|
||||
if ( !ret.next_state ) {
|
||||
if ( ch >= 0xA0 ) {
|
||||
ret = this->input_state_rule( 0x41 );
|
||||
} else {
|
||||
ret = this->input_state_rule( ch );
|
||||
}
|
||||
}
|
||||
|
||||
ret.action->char_present = true;
|
||||
ret.action->ch = ch;
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool C0_prime( wchar_t ch )
|
||||
{
|
||||
return ( (ch <= 0x17)
|
||||
|| (ch == 0x19)
|
||||
|| ( (0x1C <= ch) && (ch <= 0x1F) ) );
|
||||
}
|
||||
|
||||
static bool GLGR ( wchar_t ch )
|
||||
{
|
||||
return ( ( (0x20 <= ch) && (ch <= 0x7F) ) /* GL area */
|
||||
|| ( (0xA0 <= ch) && (ch <= 0xFF) ) ); /* GR area */
|
||||
}
|
||||
|
||||
Transition Ground::input_state_rule( wchar_t ch ) const
|
||||
{
|
||||
if ( C0_prime( ch ) ) {
|
||||
return Transition( new Execute );
|
||||
}
|
||||
|
||||
if ( GLGR( ch ) ) {
|
||||
return Transition( new Print );
|
||||
}
|
||||
|
||||
return Transition();
|
||||
}
|
||||
|
||||
Action *Escape::enter( void ) const
|
||||
{
|
||||
return new Clear;
|
||||
}
|
||||
|
||||
Transition Escape::input_state_rule( wchar_t ch ) const
|
||||
{
|
||||
if ( C0_prime( ch ) ) {
|
||||
return Transition( new Execute );
|
||||
}
|
||||
|
||||
if ( (0x20 <= ch) && (ch <= 0x2F) ) {
|
||||
return Transition( new Collect, &family->s_Escape_Intermediate );
|
||||
}
|
||||
|
||||
if ( ( (0x30 <= ch) && (ch <= 0x4F) )
|
||||
|| ( (0x51 <= ch) && (ch <= 0x57) )
|
||||
|| ( ch == 0x59 )
|
||||
|| ( ch == 0x5A )
|
||||
|| ( ch == 0x5C )
|
||||
|| ( (0x60 <= ch) && (ch <= 0x7E) ) ) {
|
||||
return Transition( new Esc_Dispatch, &family->s_Ground );
|
||||
}
|
||||
|
||||
if ( ch == 0x5B ) {
|
||||
return Transition( &family->s_CSI_Entry );
|
||||
}
|
||||
|
||||
if ( ch == 0x5D ) {
|
||||
return Transition( &family->s_OSC_String );
|
||||
}
|
||||
|
||||
if ( ch == 0x50 ) {
|
||||
return Transition( &family->s_DCS_Entry );
|
||||
}
|
||||
|
||||
if ( (ch == 0x58) || (ch == 0x5E) || (ch == 0x5F) ) {
|
||||
return Transition( &family->s_SOS_PM_APC_String );
|
||||
}
|
||||
|
||||
return Transition();
|
||||
}
|
||||
|
||||
Transition Escape_Intermediate::input_state_rule( wchar_t ch ) const
|
||||
{
|
||||
if ( C0_prime( ch ) ) {
|
||||
return Transition( new Execute );
|
||||
}
|
||||
|
||||
if ( (0x20 <= ch) && (ch <= 0x2F) ) {
|
||||
return Transition( new Collect );
|
||||
}
|
||||
|
||||
if ( (0x30 <= ch) && (ch <= 0x7E) ) {
|
||||
return Transition( new Esc_Dispatch, &family->s_Ground );
|
||||
}
|
||||
|
||||
return Transition();
|
||||
}
|
||||
|
||||
Action *CSI_Entry::enter( void ) const
|
||||
{
|
||||
return new Clear;
|
||||
}
|
||||
|
||||
Transition CSI_Entry::input_state_rule( wchar_t ch ) const
|
||||
{
|
||||
if ( C0_prime( ch ) ) {
|
||||
return Transition( new Execute );
|
||||
}
|
||||
|
||||
if ( (0x40 <= ch) && (ch <= 0x7E) ) {
|
||||
return Transition( new CSI_Dispatch, &family->s_Ground );
|
||||
}
|
||||
|
||||
if ( ( (0x30 <= ch) && (ch <= 0x39) )
|
||||
|| ( ch == 0x3B ) ) {
|
||||
return Transition( new Param, &family->s_CSI_Param );
|
||||
}
|
||||
|
||||
if ( (0x3C <= ch) && (ch <= 0x3F) ) {
|
||||
return Transition( new Collect, &family->s_CSI_Param );
|
||||
}
|
||||
|
||||
if ( ch == 0x3A ) {
|
||||
return Transition( &family->s_CSI_Ignore );
|
||||
}
|
||||
|
||||
if ( (0x20 <= ch) && (ch <= 0x2F) ) {
|
||||
return Transition( new Collect, &family->s_CSI_Intermediate );
|
||||
}
|
||||
|
||||
return Transition();
|
||||
}
|
||||
|
||||
Transition CSI_Param::input_state_rule( wchar_t ch ) const
|
||||
{
|
||||
if ( C0_prime( ch ) ) {
|
||||
return Transition( new Execute );
|
||||
}
|
||||
|
||||
if ( ( (0x30 <= ch) && (ch <= 0x39) ) || ( ch == 0x3B ) ) {
|
||||
return Transition( new Param );
|
||||
}
|
||||
|
||||
if ( ( ch == 0x3A ) || ( (0x3C <= ch) && (ch <= 0x3F) ) ) {
|
||||
return Transition( &family->s_CSI_Ignore );
|
||||
}
|
||||
|
||||
if ( (0x20 <= ch) && (ch <= 0x2F) ) {
|
||||
return Transition( new Collect, &family->s_CSI_Intermediate );
|
||||
}
|
||||
|
||||
if ( (0x40 <= ch) && (ch <= 0x7E) ) {
|
||||
return Transition( new CSI_Dispatch, &family->s_Ground );
|
||||
}
|
||||
|
||||
return Transition();
|
||||
}
|
||||
|
||||
Transition CSI_Intermediate::input_state_rule( wchar_t ch ) const
|
||||
{
|
||||
if ( C0_prime( ch ) ) {
|
||||
return Transition( new Execute );
|
||||
}
|
||||
|
||||
if ( (0x20 <= ch) && (ch <= 0x2F) ) {
|
||||
return Transition( new Collect );
|
||||
}
|
||||
|
||||
if ( (0x40 <= ch) && (ch <= 0x7E) ) {
|
||||
return Transition( new CSI_Dispatch, &family->s_Ground );
|
||||
}
|
||||
|
||||
if ( (0x30 <= ch) && (ch <= 0x3F) ) {
|
||||
return Transition( &family->s_CSI_Ignore );
|
||||
}
|
||||
|
||||
return Transition();
|
||||
}
|
||||
|
||||
Transition CSI_Ignore::input_state_rule( wchar_t ch ) const
|
||||
{
|
||||
if ( C0_prime( ch ) ) {
|
||||
return Transition( new Execute );
|
||||
}
|
||||
|
||||
if ( (0x40 <= ch) && (ch <= 0x7E) ) {
|
||||
return Transition( &family->s_Ground );
|
||||
}
|
||||
|
||||
return Transition();
|
||||
}
|
||||
|
||||
Action *DCS_Entry::enter( void ) const
|
||||
{
|
||||
return new Clear;
|
||||
}
|
||||
|
||||
Transition DCS_Entry::input_state_rule( wchar_t ch ) const
|
||||
{
|
||||
if ( (0x20 <= ch) && (ch <= 0x2F) ) {
|
||||
return Transition( new Collect, &family->s_DCS_Intermediate );
|
||||
}
|
||||
|
||||
if ( ch == 0x3A ) {
|
||||
return Transition( &family->s_DCS_Ignore );
|
||||
}
|
||||
|
||||
if ( ( (0x30 <= ch) && (ch <= 0x39) ) || ( ch == 0x3B ) ) {
|
||||
return Transition( new Param, &family->s_DCS_Param );
|
||||
}
|
||||
|
||||
if ( (0x3C <= ch) && (ch <= 0x3F) ) {
|
||||
return Transition( new Collect, &family->s_DCS_Param );
|
||||
}
|
||||
|
||||
if ( (0x40 <= ch) && (ch <= 0x7E) ) {
|
||||
return Transition( &family->s_DCS_Passthrough );
|
||||
}
|
||||
|
||||
return Transition();
|
||||
}
|
||||
|
||||
Transition DCS_Param::input_state_rule( wchar_t ch ) const
|
||||
{
|
||||
if ( ( (0x30 <= ch) && (ch <= 0x39) ) || ( ch == 0x3B ) ) {
|
||||
return Transition( new Param );
|
||||
}
|
||||
|
||||
if ( ( ch == 0x3A ) || ( (0x3C <= ch) && (ch <= 0x3F) ) ) {
|
||||
return Transition( &family->s_DCS_Ignore );
|
||||
}
|
||||
|
||||
if ( (0x20 <= ch) && (ch <= 0x2F) ) {
|
||||
return Transition( new Collect, &family->s_DCS_Intermediate );
|
||||
}
|
||||
|
||||
if ( (0x40 <= ch) && (ch <= 0x7E) ) {
|
||||
return Transition( &family->s_DCS_Passthrough );
|
||||
}
|
||||
|
||||
return Transition();
|
||||
}
|
||||
|
||||
Transition DCS_Intermediate::input_state_rule( wchar_t ch ) const
|
||||
{
|
||||
if ( (0x20 <= ch) && (ch <= 0x2F) ) {
|
||||
return Transition( new Collect );
|
||||
}
|
||||
|
||||
if ( (0x40 <= ch) && (ch <= 0x7E) ) {
|
||||
return Transition( &family->s_DCS_Passthrough );
|
||||
}
|
||||
|
||||
if ( (0x30 <= ch) && (ch <= 0x3F) ) {
|
||||
return Transition( &family->s_DCS_Ignore );
|
||||
}
|
||||
|
||||
return Transition();
|
||||
}
|
||||
|
||||
Action *DCS_Passthrough::enter( void ) const
|
||||
{
|
||||
return new Hook;
|
||||
}
|
||||
|
||||
Action *DCS_Passthrough::exit( void ) const
|
||||
{
|
||||
return new Unhook;
|
||||
}
|
||||
|
||||
Transition DCS_Passthrough::input_state_rule( wchar_t ch ) const
|
||||
{
|
||||
if ( C0_prime( ch ) || ( (0x20 <= ch) && (ch <= 0x7E) ) ) {
|
||||
return Transition( new Put );
|
||||
}
|
||||
|
||||
if ( ch == 0x9C ) {
|
||||
return Transition( &family->s_Ground );
|
||||
}
|
||||
|
||||
return Transition();
|
||||
}
|
||||
|
||||
Transition DCS_Ignore::input_state_rule( wchar_t ch ) const
|
||||
{
|
||||
if ( ch == 0x9C ) {
|
||||
return Transition( &family->s_Ground );
|
||||
}
|
||||
|
||||
return Transition();
|
||||
}
|
||||
|
||||
Action *OSC_String::enter( void ) const
|
||||
{
|
||||
return new OSC_Start;
|
||||
}
|
||||
|
||||
Action *OSC_String::exit( void ) const
|
||||
{
|
||||
return new OSC_End;
|
||||
}
|
||||
|
||||
Transition OSC_String::input_state_rule( wchar_t ch ) const
|
||||
{
|
||||
if ( (0x20 <= ch) && (ch <= 0x7F) ) {
|
||||
return Transition( new OSC_Put );
|
||||
}
|
||||
|
||||
if ( (ch == 0x9C) || (ch == 0x07) ) { /* 0x07 is xterm non-ANSI variant */
|
||||
return Transition( &family->s_Ground );
|
||||
}
|
||||
|
||||
return Transition();
|
||||
}
|
||||
|
||||
Transition SOS_PM_APC_String::input_state_rule( wchar_t ch ) const
|
||||
{
|
||||
if ( ch == 0x9C ) {
|
||||
return Transition( &family->s_Ground );
|
||||
}
|
||||
|
||||
return Transition();
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
#ifndef PARSERSTATE_HPP
|
||||
#define PARSERSTATE_HPP
|
||||
|
||||
#include "parsertransition.hpp"
|
||||
|
||||
namespace Parser {
|
||||
class StateFamily;
|
||||
|
||||
class State
|
||||
{
|
||||
protected:
|
||||
virtual Transition input_state_rule( wchar_t ch ) const = 0;
|
||||
StateFamily *family;
|
||||
|
||||
private:
|
||||
Transition anywhere_rule( wchar_t ch ) const;
|
||||
|
||||
public:
|
||||
void setfamily( StateFamily *s_family ) { family = s_family; }
|
||||
Transition input( wchar_t ch ) const;
|
||||
virtual Action *enter( void ) const { return new Ignore; }
|
||||
virtual Action *exit( void ) const { return new Ignore; }
|
||||
|
||||
State() : family( NULL ) {};
|
||||
virtual ~State() {};
|
||||
|
||||
State( const State & );
|
||||
State & operator=( const State & );
|
||||
};
|
||||
|
||||
class Ground : public State {
|
||||
Transition input_state_rule( wchar_t ch ) const;
|
||||
};
|
||||
|
||||
class Escape : public State {
|
||||
Action *enter( void ) const;
|
||||
Transition input_state_rule( wchar_t ch ) const;
|
||||
};
|
||||
|
||||
class Escape_Intermediate : public State {
|
||||
Transition input_state_rule( wchar_t ch ) const;
|
||||
};
|
||||
|
||||
class CSI_Entry : public State {
|
||||
Action *enter( void ) const;
|
||||
Transition input_state_rule( wchar_t ch ) const;
|
||||
};
|
||||
class CSI_Param : public State {
|
||||
Transition input_state_rule( wchar_t ch ) const;
|
||||
};
|
||||
class CSI_Intermediate : public State {
|
||||
Transition input_state_rule( wchar_t ch ) const;
|
||||
};
|
||||
class CSI_Ignore : public State {
|
||||
Transition input_state_rule( wchar_t ch ) const;
|
||||
};
|
||||
|
||||
class DCS_Entry : public State {
|
||||
Action *enter( void ) const;
|
||||
Transition input_state_rule( wchar_t ch ) const;
|
||||
};
|
||||
class DCS_Param : public State {
|
||||
Transition input_state_rule( wchar_t ch ) const;
|
||||
};
|
||||
class DCS_Intermediate : public State {
|
||||
Transition input_state_rule( wchar_t ch ) const;
|
||||
};
|
||||
class DCS_Passthrough : public State {
|
||||
Action *enter( void ) const;
|
||||
Transition input_state_rule( wchar_t ch ) const;
|
||||
Action *exit( void ) const;
|
||||
};
|
||||
class DCS_Ignore : public State {
|
||||
Transition input_state_rule( wchar_t ch ) const;
|
||||
};
|
||||
|
||||
class OSC_String : public State {
|
||||
Action *enter( void ) const;
|
||||
Transition input_state_rule( wchar_t ch ) const;
|
||||
Action *exit( void ) const;
|
||||
};
|
||||
class SOS_PM_APC_String : public State {
|
||||
Transition input_state_rule( wchar_t ch ) const;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,54 @@
|
||||
#ifndef PARSERSTATEFAMILY_HPP
|
||||
#define PARSERSTATEFAMILY_HPP
|
||||
|
||||
#include "parserstate.hpp"
|
||||
|
||||
namespace Parser {
|
||||
class StateFamily
|
||||
{
|
||||
public:
|
||||
Ground s_Ground;
|
||||
|
||||
Escape s_Escape;
|
||||
Escape_Intermediate s_Escape_Intermediate;
|
||||
|
||||
CSI_Entry s_CSI_Entry;
|
||||
CSI_Param s_CSI_Param;
|
||||
CSI_Intermediate s_CSI_Intermediate;
|
||||
CSI_Ignore s_CSI_Ignore;
|
||||
|
||||
DCS_Entry s_DCS_Entry;
|
||||
DCS_Param s_DCS_Param;
|
||||
DCS_Intermediate s_DCS_Intermediate;
|
||||
DCS_Passthrough s_DCS_Passthrough;
|
||||
DCS_Ignore s_DCS_Ignore;
|
||||
|
||||
OSC_String s_OSC_String;
|
||||
SOS_PM_APC_String s_SOS_PM_APC_String;
|
||||
|
||||
StateFamily()
|
||||
: s_Ground(), s_Escape(), s_Escape_Intermediate(),
|
||||
s_CSI_Entry(), s_CSI_Param(), s_CSI_Intermediate(), s_CSI_Ignore(),
|
||||
s_DCS_Entry(), s_DCS_Param(), s_DCS_Intermediate(),
|
||||
s_DCS_Passthrough(), s_DCS_Ignore(),
|
||||
s_OSC_String(), s_SOS_PM_APC_String()
|
||||
{
|
||||
s_Ground.setfamily( this );
|
||||
s_Escape.setfamily( this );
|
||||
s_Escape_Intermediate.setfamily( this );
|
||||
s_CSI_Entry.setfamily( this );
|
||||
s_CSI_Param.setfamily( this );
|
||||
s_CSI_Intermediate.setfamily( this );
|
||||
s_CSI_Ignore.setfamily( this );
|
||||
s_DCS_Entry.setfamily( this );
|
||||
s_DCS_Param.setfamily( this );
|
||||
s_DCS_Intermediate.setfamily( this );
|
||||
s_DCS_Passthrough.setfamily( this );
|
||||
s_DCS_Ignore.setfamily( this );
|
||||
s_OSC_String.setfamily( this );
|
||||
s_SOS_PM_APC_String.setfamily( this );
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,39 @@
|
||||
#ifndef PARSERTRANSITION_HPP
|
||||
#define PARSERTRANSITION_HPP
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "parseraction.hpp"
|
||||
|
||||
namespace Parser {
|
||||
class State;
|
||||
|
||||
class Transition
|
||||
{
|
||||
public:
|
||||
Action *action;
|
||||
State *next_state;
|
||||
|
||||
Transition( const Transition &x )
|
||||
: action( x.action ),
|
||||
next_state( x.next_state ) {}
|
||||
Transition & operator=( const Transition &t )
|
||||
{
|
||||
action = t.action;
|
||||
next_state = t.next_state;
|
||||
|
||||
return *this;
|
||||
}
|
||||
virtual ~Transition() {}
|
||||
|
||||
Transition( Action *s_action=new Ignore, State *s_next_state=NULL )
|
||||
: action( s_action ), next_state( s_next_state )
|
||||
{}
|
||||
|
||||
Transition( State *s_next_state )
|
||||
: action( new Ignore ), next_state( s_next_state )
|
||||
{}
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -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.hpp"
|
||||
#include "swrite.hpp"
|
||||
#include "completeterminal.hpp"
|
||||
#include "user.hpp"
|
||||
|
||||
#include "networktransport.cpp"
|
||||
|
||||
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.hpp"
|
||||
#include "networktransport.hpp"
|
||||
#include "user.hpp"
|
||||
#include "terminaloverlay.hpp"
|
||||
|
||||
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,23 @@
|
||||
#include <unistd.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "swrite.hpp"
|
||||
|
||||
int swrite( int fd, const char *str, ssize_t len )
|
||||
{
|
||||
ssize_t total_bytes_written = 0;
|
||||
ssize_t bytes_to_write = ( len >= 0 ) ? len : strlen( str );
|
||||
while ( total_bytes_written < bytes_to_write ) {
|
||||
ssize_t bytes_written = write( fd, str + total_bytes_written,
|
||||
bytes_to_write - total_bytes_written );
|
||||
if ( bytes_written <= 0 ) {
|
||||
perror( "write" );
|
||||
return -1;
|
||||
} else {
|
||||
total_bytes_written += bytes_written;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#ifndef SWRITE_HPP
|
||||
#define SWRITE_HPP
|
||||
|
||||
int swrite( int fd, const char *str, ssize_t len = -1 );
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,155 @@
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <typeinfo>
|
||||
|
||||
#include "terminal.hpp"
|
||||
#include "swrite.hpp"
|
||||
|
||||
using namespace Terminal;
|
||||
|
||||
Emulator::Emulator( size_t s_width, size_t s_height )
|
||||
: fb( s_width, s_height ), dispatch(), user()
|
||||
{}
|
||||
|
||||
std::string Emulator::read_octets_to_host( void )
|
||||
{
|
||||
std::string ret = dispatch.terminal_to_host;
|
||||
dispatch.terminal_to_host.clear();
|
||||
return ret;
|
||||
}
|
||||
|
||||
void Emulator::execute( const Parser::Execute *act )
|
||||
{
|
||||
dispatch.dispatch( CONTROL, act, &fb );
|
||||
}
|
||||
|
||||
void Emulator::print( const Parser::Print *act )
|
||||
{
|
||||
assert( act->char_present );
|
||||
|
||||
int chwidth = act->ch == L'\0' ? -1 : wcwidth( act->ch );
|
||||
|
||||
Cell *this_cell = fb.get_mutable_cell();
|
||||
|
||||
Cell *combining_cell = fb.get_combining_cell(); /* can be null if we were resized */
|
||||
|
||||
switch ( chwidth ) {
|
||||
case 1: /* normal character */
|
||||
case 2: /* wide character */
|
||||
if ( fb.ds.auto_wrap_mode && fb.ds.next_print_will_wrap ) {
|
||||
fb.get_mutable_row( -1 )->wrap = true;
|
||||
fb.ds.move_col( 0 );
|
||||
fb.move_rows_autoscroll( 1 );
|
||||
}
|
||||
|
||||
/* wrap 2-cell chars if no room, even without will-wrap flag */
|
||||
if ( fb.ds.auto_wrap_mode
|
||||
&& (chwidth == 2)
|
||||
&& (fb.ds.get_cursor_col() == fb.ds.get_width() - 1) ) {
|
||||
fb.reset_cell( this_cell );
|
||||
fb.get_mutable_row( -1 )->wrap = false;
|
||||
/* There doesn't seem to be a consistent way to get the
|
||||
downstream terminal emulator to set the wrap-around
|
||||
copy-and-paste flag on a row that ends with an empty cell
|
||||
because a wide char was wrapped to the next line. */
|
||||
fb.ds.move_col( 0 );
|
||||
fb.move_rows_autoscroll( 1 );
|
||||
}
|
||||
|
||||
if ( fb.ds.insert_mode ) {
|
||||
for ( int i = 0; i < chwidth; i++ ) {
|
||||
fb.insert_cell( fb.ds.get_cursor_row(), fb.ds.get_cursor_col() );
|
||||
}
|
||||
}
|
||||
|
||||
this_cell = fb.get_mutable_cell();
|
||||
|
||||
fb.reset_cell( this_cell );
|
||||
this_cell->contents.push_back( act->ch );
|
||||
this_cell->width = chwidth;
|
||||
fb.apply_renditions_to_current_cell();
|
||||
|
||||
if ( chwidth == 2 ) { /* erase overlapped cell */
|
||||
if ( fb.ds.get_cursor_col() + 1 < fb.ds.get_width() ) {
|
||||
fb.reset_cell( fb.get_mutable_cell( fb.ds.get_cursor_row(), fb.ds.get_cursor_col() + 1 ) );
|
||||
}
|
||||
}
|
||||
|
||||
fb.ds.move_col( chwidth, true, true );
|
||||
|
||||
act->handled = true;
|
||||
break;
|
||||
case 0: /* combining character */
|
||||
if ( combining_cell == NULL ) { /* character is now offscreen */
|
||||
act->handled = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if ( combining_cell->contents.size() == 0 ) {
|
||||
/* cell starts with combining character */
|
||||
assert( this_cell == combining_cell );
|
||||
assert( combining_cell->width == 1 );
|
||||
combining_cell->fallback = true;
|
||||
fb.ds.move_col( 1, true, true );
|
||||
}
|
||||
|
||||
if ( combining_cell->contents.size() < 16 ) {
|
||||
/* seems like a reasonable limit on combining characters */
|
||||
combining_cell->contents.push_back( act->ch );
|
||||
}
|
||||
act->handled = true;
|
||||
break;
|
||||
case -1: /* unprintable character */
|
||||
break;
|
||||
default:
|
||||
assert( false );
|
||||
}
|
||||
}
|
||||
|
||||
void Emulator::CSI_dispatch( const Parser::CSI_Dispatch *act )
|
||||
{
|
||||
dispatch.dispatch( CSI, act, &fb );
|
||||
}
|
||||
|
||||
void Emulator::OSC_end( const Parser::OSC_End *act )
|
||||
{
|
||||
dispatch.OSC_dispatch( act, &fb );
|
||||
}
|
||||
|
||||
void Emulator::Esc_dispatch( const Parser::Esc_Dispatch *act )
|
||||
{
|
||||
/* handle 7-bit ESC-encoding of C1 control characters */
|
||||
if ( (dispatch.get_dispatch_chars().size() == 0)
|
||||
&& (0x40 <= act->ch)
|
||||
&& (act->ch <= 0x5F) ) {
|
||||
Parser::Esc_Dispatch act2 = *act;
|
||||
act2.ch += 0x40;
|
||||
dispatch.dispatch( CONTROL, &act2, &fb );
|
||||
} else {
|
||||
dispatch.dispatch( ESCAPE, act, &fb );
|
||||
}
|
||||
}
|
||||
|
||||
std::string Emulator::open( void )
|
||||
{
|
||||
char appmode[ 6 ] = { 0x1b, '[', '?', '1', 'h', 0 };
|
||||
return std::string( appmode );
|
||||
}
|
||||
|
||||
std::string Emulator::close( void )
|
||||
{
|
||||
return std::string( "\033[?1l\033[!p" );
|
||||
}
|
||||
|
||||
void Emulator::resize( size_t s_width, size_t s_height )
|
||||
{
|
||||
fb.resize( s_width, s_height );
|
||||
}
|
||||
|
||||
bool Emulator::operator==( Emulator const &x ) const
|
||||
{
|
||||
/* dispatcher and user are irrelevant for us */
|
||||
return ( fb == x.fb );
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
#ifndef TERMINAL_CPP
|
||||
#define TERMINAL_CPP
|
||||
|
||||
#include <wchar.h>
|
||||
#include <stdio.h>
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
|
||||
#include "parseraction.hpp"
|
||||
#include "terminalframebuffer.hpp"
|
||||
#include "terminaldispatcher.hpp"
|
||||
#include "terminaluserinput.hpp"
|
||||
#include "terminaldisplay.hpp"
|
||||
|
||||
namespace Terminal {
|
||||
class Emulator {
|
||||
friend void Parser::Print::act_on_terminal( Emulator * ) const;
|
||||
friend void Parser::Execute::act_on_terminal( Emulator * ) const;
|
||||
friend void Parser::Clear::act_on_terminal( Emulator * ) const;
|
||||
friend void Parser::Param::act_on_terminal( Emulator * ) const;
|
||||
friend void Parser::Collect::act_on_terminal( Emulator * ) const;
|
||||
friend void Parser::CSI_Dispatch::act_on_terminal( Emulator * ) const;
|
||||
friend void Parser::Esc_Dispatch::act_on_terminal( Emulator * ) const;
|
||||
friend void Parser::OSC_Start::act_on_terminal( Emulator * ) const;
|
||||
friend void Parser::OSC_Put::act_on_terminal( Emulator * ) const;
|
||||
friend void Parser::OSC_End::act_on_terminal( Emulator * ) const;
|
||||
|
||||
friend void Parser::UserByte::act_on_terminal( Emulator * ) const;
|
||||
friend void Parser::Resize::act_on_terminal( Emulator * ) const;
|
||||
|
||||
private:
|
||||
Framebuffer fb;
|
||||
Dispatcher dispatch;
|
||||
UserInput user;
|
||||
|
||||
/* action methods */
|
||||
void print( const Parser::Print *act );
|
||||
void execute( const Parser::Execute *act );
|
||||
void CSI_dispatch( const Parser::CSI_Dispatch *act );
|
||||
void Esc_dispatch( const Parser::Esc_Dispatch *act );
|
||||
void OSC_end( const Parser::OSC_End *act );
|
||||
void resize( size_t s_width, size_t s_height );
|
||||
|
||||
public:
|
||||
Emulator( size_t s_width, size_t s_height );
|
||||
|
||||
std::string read_octets_to_host( void );
|
||||
|
||||
static std::string open( void ); /* put user cursor keys in application mode */
|
||||
static std::string close( void ); /* restore user cursor keys */
|
||||
|
||||
const Framebuffer & get_fb( void ) const { return fb; }
|
||||
|
||||
bool operator==( Emulator const &x ) const;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,211 @@
|
||||
#include <stdio.h>
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "terminaldispatcher.hpp"
|
||||
#include "parseraction.hpp"
|
||||
#include "terminalframebuffer.hpp"
|
||||
|
||||
using namespace Terminal;
|
||||
|
||||
Dispatcher::Dispatcher()
|
||||
: params(), parsed_params(), parsed( false ), dispatch_chars(),
|
||||
OSC_string(), terminal_to_host()
|
||||
{}
|
||||
|
||||
void Dispatcher::newparamchar( const Parser::Param *act )
|
||||
{
|
||||
assert( act->char_present );
|
||||
assert( (act->ch == ';') || ( (act->ch >= '0') && (act->ch <= '9') ) );
|
||||
if ( params.length() < 100 ) {
|
||||
/* enough for 16 five-char params plus 15 semicolons */
|
||||
params.push_back( act->ch );
|
||||
act->handled = true;
|
||||
}
|
||||
parsed = false;
|
||||
}
|
||||
|
||||
void Dispatcher::collect( const Parser::Collect *act )
|
||||
{
|
||||
assert( act->char_present );
|
||||
if ( ( dispatch_chars.length() < 8 ) /* never should need more than 2 */
|
||||
&& ( act->ch <= 255 ) ) { /* ignore non-8-bit */
|
||||
dispatch_chars.push_back( act->ch );
|
||||
act->handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
void Dispatcher::clear( const Parser::Clear *act )
|
||||
{
|
||||
params.clear();
|
||||
dispatch_chars.clear();
|
||||
parsed = false;
|
||||
act->handled = true;
|
||||
}
|
||||
|
||||
void Dispatcher::parse_params( void )
|
||||
{
|
||||
if ( parsed ) {
|
||||
return;
|
||||
}
|
||||
|
||||
parsed_params.clear();
|
||||
const char *str = params.c_str();
|
||||
const char *segment_begin = str;
|
||||
|
||||
while ( 1 ) {
|
||||
const char *segment_end = strchr( segment_begin, ';' );
|
||||
if ( segment_end == NULL ) {
|
||||
break;
|
||||
}
|
||||
|
||||
errno = 0;
|
||||
char *endptr;
|
||||
int val = strtol( segment_begin, &endptr, 10 );
|
||||
if ( endptr == segment_begin ) {
|
||||
val = -1;
|
||||
}
|
||||
if ( errno == 0 ) {
|
||||
parsed_params.push_back( val );
|
||||
}
|
||||
|
||||
segment_begin = segment_end + 1;
|
||||
}
|
||||
|
||||
/* get last param */
|
||||
errno = 0;
|
||||
char *endptr;
|
||||
int val = strtol( segment_begin, &endptr, 10 );
|
||||
if ( endptr == segment_begin ) {
|
||||
val = -1;
|
||||
}
|
||||
if ( errno == 0 ) {
|
||||
parsed_params.push_back( val );
|
||||
}
|
||||
|
||||
parsed = true;
|
||||
}
|
||||
|
||||
int Dispatcher::getparam( size_t N, int defaultval )
|
||||
{
|
||||
int ret = defaultval;
|
||||
if ( !parsed ) {
|
||||
parse_params();
|
||||
}
|
||||
|
||||
if ( parsed_params.size() > N ) {
|
||||
ret = parsed_params[ N ];
|
||||
}
|
||||
if ( ret < 1 ) ret = defaultval;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
int Dispatcher::param_count( void )
|
||||
{
|
||||
if ( !parsed ) {
|
||||
parse_params();
|
||||
}
|
||||
|
||||
return parsed_params.size();
|
||||
}
|
||||
|
||||
std::string Dispatcher::str( void )
|
||||
{
|
||||
char assum[ 64 ];
|
||||
snprintf( assum, 64, "[dispatch=\"%s\" params=\"%s\"]",
|
||||
dispatch_chars.c_str(), params.c_str() );
|
||||
return std::string( assum );
|
||||
}
|
||||
|
||||
/* construct on first use to avoid static initialization order crash */
|
||||
DispatchRegistry & Terminal::get_global_dispatch_registry( void )
|
||||
{
|
||||
static DispatchRegistry global_dispatch_registry;
|
||||
return global_dispatch_registry;
|
||||
}
|
||||
|
||||
static void register_function( Function_Type type,
|
||||
std::string dispatch_chars,
|
||||
Function f )
|
||||
{
|
||||
switch ( type ) {
|
||||
case ESCAPE:
|
||||
get_global_dispatch_registry().escape.insert( dispatch_map_t::value_type( dispatch_chars, f ) );
|
||||
break;
|
||||
case CSI:
|
||||
get_global_dispatch_registry().CSI.insert( dispatch_map_t::value_type( dispatch_chars, f ) );
|
||||
break;
|
||||
case CONTROL:
|
||||
get_global_dispatch_registry().control.insert( dispatch_map_t::value_type( dispatch_chars, f ) );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Function::Function( Function_Type type, std::string dispatch_chars,
|
||||
void (*s_function)( Framebuffer *, Dispatcher * ),
|
||||
bool s_clears_wrap_state )
|
||||
: function( s_function ), clears_wrap_state( s_clears_wrap_state )
|
||||
{
|
||||
register_function( type, dispatch_chars, *this );
|
||||
}
|
||||
|
||||
void Dispatcher::dispatch( Function_Type type, const Parser::Action *act, Framebuffer *fb )
|
||||
{
|
||||
/* add final char to dispatch key */
|
||||
if ( (type == ESCAPE) || (type == CSI) ) {
|
||||
assert( act->char_present );
|
||||
Parser::Collect act2;
|
||||
act2.char_present = true;
|
||||
act2.ch = act->ch;
|
||||
collect( &act2 );
|
||||
}
|
||||
|
||||
dispatch_map_t *map = NULL;
|
||||
switch ( type ) {
|
||||
case ESCAPE: map = &get_global_dispatch_registry().escape; break;
|
||||
case CSI: map = &get_global_dispatch_registry().CSI; break;
|
||||
case CONTROL: map = &get_global_dispatch_registry().control; break;
|
||||
}
|
||||
|
||||
std::string key = dispatch_chars;
|
||||
if ( type == CONTROL ) {
|
||||
assert( act->ch <= 255 );
|
||||
char ctrlstr[ 2 ] = { (char)act->ch, 0 };
|
||||
key = std::string( ctrlstr, 1 );
|
||||
}
|
||||
|
||||
dispatch_map_t::const_iterator i = map->find( key );
|
||||
if ( i == map->end() ) {
|
||||
/* unknown function */
|
||||
fb->ds.next_print_will_wrap = false;
|
||||
return;
|
||||
} else {
|
||||
act->handled = true;
|
||||
if ( i->second.clears_wrap_state ) {
|
||||
fb->ds.next_print_will_wrap = false;
|
||||
}
|
||||
return i->second.function( fb, this );
|
||||
}
|
||||
}
|
||||
|
||||
void Dispatcher::OSC_put( const Parser::OSC_Put *act )
|
||||
{
|
||||
assert( act->char_present );
|
||||
if ( OSC_string.size() < 256 ) { /* should be a long enough window title */
|
||||
OSC_string.push_back( act->ch );
|
||||
act->handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
void Dispatcher::OSC_start( const Parser::OSC_Start *act )
|
||||
{
|
||||
OSC_string.clear();
|
||||
act->handled = true;
|
||||
}
|
||||
|
||||
bool Dispatcher::operator==( const Dispatcher &x ) const
|
||||
{
|
||||
return ( params == x.params ) && ( parsed_params == x.parsed_params ) && ( parsed == x.parsed )
|
||||
&& ( dispatch_chars == x.dispatch_chars ) && ( OSC_string == x.OSC_string ) && ( terminal_to_host == x.terminal_to_host );
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
#ifndef TERMINALDISPATCHER_HPP
|
||||
#define TERMINALDISPATCHER_HPP
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <map>
|
||||
|
||||
namespace Parser {
|
||||
class Action;
|
||||
class Param;
|
||||
class Collect;
|
||||
class Clear;
|
||||
class Esc_Dispatch;
|
||||
class CSI_Dispatch;
|
||||
class Execute;
|
||||
class OSC_Start;
|
||||
class OSC_Put;
|
||||
class OSC_End;
|
||||
}
|
||||
|
||||
namespace Terminal {
|
||||
class Framebuffer;
|
||||
class Dispatcher;
|
||||
|
||||
enum Function_Type { ESCAPE, CSI, CONTROL };
|
||||
|
||||
class Function {
|
||||
public:
|
||||
Function() : function( NULL ), clears_wrap_state( true ) {}
|
||||
Function( Function_Type type, std::string dispatch_chars,
|
||||
void (*s_function)( Framebuffer *, Dispatcher * ),
|
||||
bool s_clears_wrap_state = true );
|
||||
void (*function)( Framebuffer *, Dispatcher * );
|
||||
bool clears_wrap_state;
|
||||
};
|
||||
|
||||
typedef std::map<std::string, Function> dispatch_map_t;
|
||||
|
||||
class DispatchRegistry {
|
||||
public:
|
||||
dispatch_map_t escape;
|
||||
dispatch_map_t CSI;
|
||||
dispatch_map_t control;
|
||||
|
||||
DispatchRegistry() : escape(), CSI(), control() {}
|
||||
};
|
||||
|
||||
DispatchRegistry & get_global_dispatch_registry( void );
|
||||
|
||||
class Dispatcher {
|
||||
private:
|
||||
std::string params;
|
||||
std::vector<int> parsed_params;
|
||||
bool parsed;
|
||||
|
||||
std::string dispatch_chars;
|
||||
std::vector<wchar_t> OSC_string; /* only used to set the window title */
|
||||
|
||||
void parse_params( void );
|
||||
|
||||
public:
|
||||
std::string terminal_to_host; /* this is the reply string */
|
||||
|
||||
Dispatcher();
|
||||
int getparam( size_t N, int defaultval );
|
||||
int param_count( void );
|
||||
|
||||
void newparamchar( const Parser::Param *act );
|
||||
void collect( const Parser::Collect *act );
|
||||
void clear( const Parser::Clear *act );
|
||||
|
||||
std::string str( void );
|
||||
|
||||
void dispatch( Function_Type type, const Parser::Action *act, Framebuffer *fb );
|
||||
std::string get_dispatch_chars( void ) { return dispatch_chars; }
|
||||
std::vector<wchar_t> get_OSC_string( void ) { return OSC_string; }
|
||||
|
||||
void OSC_put( const Parser::OSC_Put *act );
|
||||
void OSC_start( const Parser::OSC_Start *act );
|
||||
void OSC_dispatch( const Parser::OSC_End *act, Framebuffer *fb );
|
||||
|
||||
bool operator==( const Dispatcher &x ) const;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,259 @@
|
||||
#include <assert.h>
|
||||
|
||||
#include "terminaldisplay.hpp"
|
||||
|
||||
using namespace Terminal;
|
||||
|
||||
/* Print a new "frame" to the terminal, using ANSI/ECMA-48 escape codes. */
|
||||
|
||||
std::string Display::new_frame( bool initialized, const Framebuffer &last, const Framebuffer &f )
|
||||
{
|
||||
FrameState frame( last );
|
||||
|
||||
char tmp[ 64 ];
|
||||
|
||||
/* has bell been rung? */
|
||||
if ( f.get_bell_count() != frame.last_frame.get_bell_count() ) {
|
||||
frame.append( "\x07" );
|
||||
}
|
||||
|
||||
/* has window title changed? */
|
||||
if ( (!initialized)
|
||||
|| (f.get_window_title() != frame.last_frame.get_window_title()) ) {
|
||||
/* set window title */
|
||||
frame.append( "\033]0;" );
|
||||
const std::deque<wchar_t> &window_title( f.get_window_title() );
|
||||
for ( auto i = window_title.begin();
|
||||
i != window_title.end();
|
||||
i++ ) {
|
||||
snprintf( tmp, 64, "%lc", *i );
|
||||
frame.append( tmp );
|
||||
}
|
||||
frame.append( "\033\\" );
|
||||
}
|
||||
|
||||
/* has reverse video state changed? */
|
||||
if ( (!initialized)
|
||||
|| (f.ds.reverse_video != frame.last_frame.ds.reverse_video) ) {
|
||||
/* set reverse video */
|
||||
snprintf( tmp, 64, "\033[?5%c", (f.ds.reverse_video ? 'h' : 'l') );
|
||||
frame.append( tmp );
|
||||
}
|
||||
|
||||
/* has size changed? */
|
||||
if ( (!initialized)
|
||||
|| (f.ds.get_width() != frame.last_frame.ds.get_width())
|
||||
|| (f.ds.get_height() != frame.last_frame.ds.get_height()) ) {
|
||||
/* clear screen */
|
||||
frame.append( "\033[0m\033[H\033[2J" );
|
||||
initialized = false;
|
||||
frame.cursor_x = frame.cursor_y = 0;
|
||||
frame.current_rendition_string = "\033[0m";
|
||||
} else {
|
||||
frame.cursor_x = frame.last_frame.ds.get_cursor_col();
|
||||
frame.cursor_y = frame.last_frame.ds.get_cursor_row();
|
||||
frame.current_rendition_string = frame.last_frame.ds.get_renditions().sgr();
|
||||
}
|
||||
|
||||
/* shortcut -- has display moved up by a certain number of lines? */
|
||||
frame.y = 0;
|
||||
|
||||
if ( initialized ) {
|
||||
int lines_scrolled = 0;
|
||||
int scroll_height = 0;
|
||||
|
||||
for ( int row = 0; row < f.ds.get_height(); row++ ) {
|
||||
if ( *(f.get_row( 0 )) == *(frame.last_frame.get_row( row )) ) {
|
||||
/* found a scroll */
|
||||
lines_scrolled = row;
|
||||
scroll_height = 1;
|
||||
|
||||
/* how big is the region that was scrolled? */
|
||||
for ( int region_height = 1;
|
||||
lines_scrolled + region_height < f.ds.get_height();
|
||||
region_height++ ) {
|
||||
if ( *(f.get_row( region_height ))
|
||||
== *(frame.last_frame.get_row( lines_scrolled + region_height )) ) {
|
||||
scroll_height = region_height + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( scroll_height ) {
|
||||
frame.y = scroll_height;
|
||||
|
||||
if ( lines_scrolled ) {
|
||||
if ( frame.cursor_y != f.ds.get_height() - 1 ) {
|
||||
frame.append_silent_move( f.ds.get_height() - 1, 0 );
|
||||
}
|
||||
|
||||
if ( frame.current_rendition_string != "\033[0m" ) {
|
||||
frame.append( "\033[0m" );
|
||||
frame.current_rendition_string = "\033[0m";
|
||||
}
|
||||
|
||||
for ( int i = 0; i < lines_scrolled; i++ ) {
|
||||
frame.append( "\n" );
|
||||
}
|
||||
|
||||
for ( int i = 0; i < f.ds.get_height(); i++ ) {
|
||||
if ( i + lines_scrolled < f.ds.get_height() ) {
|
||||
*(frame.last_frame.get_mutable_row( i )) = *(frame.last_frame.get_row( i + lines_scrolled ));
|
||||
} else {
|
||||
frame.last_frame.get_mutable_row( i )->reset( 0 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* iterate for every cell */
|
||||
for ( ; frame.y < f.ds.get_height(); frame.y++ ) {
|
||||
int last_x = 0;
|
||||
for ( frame.x = 0;
|
||||
frame.x < f.ds.get_width(); /* let put_cell() handle advance */ ) {
|
||||
last_x = frame.x;
|
||||
put_cell( initialized, frame, f );
|
||||
|
||||
/* To hint that a word-select should group the end of one line
|
||||
with the beginning of the next, we let the real cursor
|
||||
actually wrap around in cases where it wrapped around for us. */
|
||||
|
||||
if ( (frame.cursor_x >= f.ds.get_width())
|
||||
&& (frame.y < f.ds.get_height() - 1)
|
||||
&& f.get_row( frame.y )->wrap
|
||||
&& (!initialized || !frame.last_frame.get_row( frame.y )->wrap) ) {
|
||||
/* next write will wrap */
|
||||
frame.cursor_x = 0;
|
||||
frame.cursor_y++;
|
||||
}
|
||||
}
|
||||
|
||||
/* Turn off wrap */
|
||||
if ( (frame.y < f.ds.get_height() - 1)
|
||||
&& (!f.get_row( frame.y )->wrap)
|
||||
&& (!initialized || frame.last_frame.get_row( frame.y )->wrap) ) {
|
||||
frame.x = last_x;
|
||||
if ( initialized ) {
|
||||
frame.last_frame.reset_cell( frame.last_frame.get_mutable_cell( frame.y, frame.x ) );
|
||||
}
|
||||
|
||||
snprintf( tmp, 64, "\033[%d;%dH\033[K", frame.y + 1, frame.x + 1 );
|
||||
frame.append( tmp );
|
||||
frame.cursor_x = frame.x;
|
||||
|
||||
put_cell( initialized, frame, f );
|
||||
}
|
||||
}
|
||||
|
||||
/* has cursor location changed? */
|
||||
if ( (!initialized)
|
||||
|| (f.ds.get_cursor_row() != frame.cursor_y)
|
||||
|| (f.ds.get_cursor_col() != frame.cursor_x) ) {
|
||||
snprintf( tmp, 64, "\033[%d;%dH", f.ds.get_cursor_row() + 1,
|
||||
f.ds.get_cursor_col() + 1 );
|
||||
frame.append( tmp );
|
||||
frame.cursor_x = f.ds.get_cursor_col();
|
||||
frame.cursor_y = f.ds.get_cursor_row();
|
||||
}
|
||||
|
||||
/* has cursor visibility changed? */
|
||||
if ( (!initialized)
|
||||
|| (f.ds.cursor_visible != frame.last_frame.ds.cursor_visible) ) {
|
||||
if ( f.ds.cursor_visible ) {
|
||||
frame.append( "\033[?25h" );
|
||||
} else {
|
||||
frame.append( "\033[?25l" );
|
||||
}
|
||||
}
|
||||
|
||||
/* have renditions changed? */
|
||||
if ( (!initialized)
|
||||
|| (f.ds.get_renditions().sgr() != frame.current_rendition_string) ) {
|
||||
frame.appendstring( f.ds.get_renditions().sgr() );
|
||||
frame.current_rendition_string = f.ds.get_renditions().sgr();
|
||||
}
|
||||
|
||||
return frame.str;
|
||||
}
|
||||
|
||||
void Display::put_cell( bool initialized, FrameState &frame, const Framebuffer &f )
|
||||
{
|
||||
char tmp[ 64 ];
|
||||
|
||||
const Cell *cell = f.get_cell( frame.y, frame.x );
|
||||
|
||||
if ( initialized
|
||||
&& ( *cell == *(frame.last_frame.get_cell( frame.y, frame.x )) ) ) {
|
||||
frame.x += cell->width;
|
||||
return;
|
||||
}
|
||||
|
||||
if ( (frame.x != frame.cursor_x) || (frame.y != frame.cursor_y) ) {
|
||||
frame.append_silent_move( frame.y, frame.x );
|
||||
}
|
||||
|
||||
std::string rendition_str = cell->renditions.sgr();
|
||||
|
||||
if ( frame.current_rendition_string != rendition_str ) {
|
||||
/* print renditions */
|
||||
frame.appendstring( rendition_str );
|
||||
frame.current_rendition_string = rendition_str;
|
||||
}
|
||||
|
||||
if ( cell->contents.empty() ) {
|
||||
/* see how far we can stretch a clear */
|
||||
int clear_count = 0;
|
||||
for ( int col = frame.x; col < f.ds.get_width(); col++ ) {
|
||||
const Cell *other_cell = f.get_cell( frame.y, col );
|
||||
if ( (cell->renditions == other_cell->renditions)
|
||||
&& (other_cell->contents.empty()) ) {
|
||||
clear_count++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
snprintf( tmp, 64, "\033[%dX", clear_count );
|
||||
frame.append( tmp );
|
||||
|
||||
frame.x += clear_count;
|
||||
return;
|
||||
}
|
||||
|
||||
/* cells that begin with combining character get combiner attached to no-break space */
|
||||
if ( cell->fallback ) {
|
||||
snprintf( tmp, 64, "%lc", 0xA0 );
|
||||
frame.append( tmp );
|
||||
}
|
||||
|
||||
for ( std::vector<wchar_t>::const_iterator i = cell->contents.begin();
|
||||
i != cell->contents.end();
|
||||
i++ ) {
|
||||
snprintf( tmp, 64, "%lc", *i );
|
||||
frame.append( tmp );
|
||||
}
|
||||
|
||||
frame.x += cell->width;
|
||||
frame.cursor_x += cell->width;
|
||||
}
|
||||
|
||||
void FrameState::append_silent_move( int y, int x )
|
||||
{
|
||||
char tmp[ 64 ];
|
||||
|
||||
/* turn off cursor if necessary before moving cursor */
|
||||
if ( last_frame.ds.cursor_visible ) {
|
||||
append( "\033[?25l" );
|
||||
last_frame.ds.cursor_visible = false;
|
||||
}
|
||||
|
||||
snprintf( tmp, 64, "\033[%d;%dH", y + 1, x + 1 );
|
||||
append( tmp );
|
||||
cursor_x = x;
|
||||
cursor_y = y;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
#ifndef TERMINALDISPLAY_HPP
|
||||
#define TERMINALDISPLAY_HPP
|
||||
|
||||
#include "terminalframebuffer.hpp"
|
||||
|
||||
namespace Terminal {
|
||||
/* variables used within a new_frame */
|
||||
class FrameState {
|
||||
public:
|
||||
int x, y;
|
||||
std::string str;
|
||||
|
||||
int cursor_x, cursor_y;
|
||||
std::string current_rendition_string;
|
||||
|
||||
Framebuffer last_frame;
|
||||
|
||||
FrameState( const Framebuffer &s_last )
|
||||
: x(0), y(0),
|
||||
str(), cursor_x(0), cursor_y(0), current_rendition_string(),
|
||||
last_frame( s_last )
|
||||
{
|
||||
str.reserve( 1024 );
|
||||
}
|
||||
|
||||
void append( const char * s ) { str.append( s ); }
|
||||
void appendstring( const std::string s ) { str.append( s ); }
|
||||
|
||||
void append_silent_move( int y, int x );
|
||||
};
|
||||
|
||||
class Display {
|
||||
private:
|
||||
static void put_cell( bool initialized, FrameState &frame, const Framebuffer &f );
|
||||
|
||||
public:
|
||||
static std::string new_frame( bool initialized, const Framebuffer &last, const Framebuffer &f );
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,425 @@
|
||||
#include <assert.h>
|
||||
|
||||
#include "terminalframebuffer.hpp"
|
||||
|
||||
using namespace Terminal;
|
||||
|
||||
void Cell::reset( int background_color )
|
||||
{
|
||||
contents.clear();
|
||||
fallback = false;
|
||||
width = 1;
|
||||
renditions = Renditions( background_color );
|
||||
}
|
||||
|
||||
DrawState::DrawState( int s_width, int s_height )
|
||||
: width( s_width ), height( s_height ),
|
||||
cursor_col( 0 ), cursor_row( 0 ),
|
||||
combining_char_col( 0 ), combining_char_row( 0 ), tabs( s_width ),
|
||||
scrolling_region_top_row( 0 ), scrolling_region_bottom_row( height - 1 ),
|
||||
renditions( 0 ), save(),
|
||||
next_print_will_wrap( false ), origin_mode( false ), auto_wrap_mode( true ),
|
||||
insert_mode( false ), cursor_visible( true ), reverse_video( false ),
|
||||
application_mode_cursor_keys( false )
|
||||
{
|
||||
for ( int i = 0; i < width; i++ ) {
|
||||
tabs[ i ] = ( (i % 8) == 0 );
|
||||
}
|
||||
}
|
||||
|
||||
Framebuffer::Framebuffer( int s_width, int s_height )
|
||||
: rows( s_height, Row( s_width, 0 ) ), window_title(), bell_count( 0 ), ds( s_width, s_height )
|
||||
{
|
||||
assert( s_height > 0 );
|
||||
assert( s_width > 0 );
|
||||
}
|
||||
|
||||
void Framebuffer::scroll( int N )
|
||||
{
|
||||
if ( N >= 0 ) {
|
||||
for ( int i = 0; i < N; i++ ) {
|
||||
delete_line( ds.get_scrolling_region_top_row() );
|
||||
ds.move_row( -1, true );
|
||||
}
|
||||
} else {
|
||||
N = -N;
|
||||
|
||||
for ( int i = 0; i < N; i++ ) {
|
||||
rows.insert( rows.begin() + ds.get_scrolling_region_top_row(), newrow() );
|
||||
rows.erase( rows.begin() + ds.get_scrolling_region_bottom_row() + 1 );
|
||||
ds.move_row( 1, true );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DrawState::new_grapheme( void )
|
||||
{
|
||||
combining_char_col = cursor_col;
|
||||
combining_char_row = cursor_row;
|
||||
}
|
||||
|
||||
void DrawState::snap_cursor_to_border( void )
|
||||
{
|
||||
if ( cursor_row < limit_top() ) cursor_row = limit_top();
|
||||
if ( cursor_row > limit_bottom() ) cursor_row = limit_bottom();
|
||||
if ( cursor_col < 0 ) cursor_col = 0;
|
||||
if ( cursor_col >= width ) cursor_col = width - 1;
|
||||
}
|
||||
|
||||
void DrawState::move_row( int N, bool relative )
|
||||
{
|
||||
if ( relative ) {
|
||||
cursor_row += N;
|
||||
} else {
|
||||
cursor_row = N + limit_top();
|
||||
}
|
||||
|
||||
snap_cursor_to_border();
|
||||
new_grapheme();
|
||||
next_print_will_wrap = false;
|
||||
}
|
||||
|
||||
void DrawState::move_col( int N, bool relative, bool implicit )
|
||||
{
|
||||
if ( implicit ) {
|
||||
new_grapheme();
|
||||
}
|
||||
|
||||
if ( relative ) {
|
||||
cursor_col += N;
|
||||
} else {
|
||||
cursor_col = N;
|
||||
}
|
||||
|
||||
if ( implicit && (cursor_col >= width) ) {
|
||||
next_print_will_wrap = true;
|
||||
}
|
||||
|
||||
snap_cursor_to_border();
|
||||
if ( !implicit ) {
|
||||
new_grapheme();
|
||||
next_print_will_wrap = false;
|
||||
}
|
||||
}
|
||||
|
||||
void Framebuffer::move_rows_autoscroll( int rows )
|
||||
{
|
||||
/* don't scroll if outside the scrolling region */
|
||||
if ( (ds.get_cursor_row() < ds.get_scrolling_region_top_row())
|
||||
|| (ds.get_cursor_row() > ds.get_scrolling_region_bottom_row()) ) {
|
||||
ds.move_row( rows, true );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ds.get_cursor_row() + rows > ds.get_scrolling_region_bottom_row() ) {
|
||||
scroll( ds.get_cursor_row() + rows - ds.get_scrolling_region_bottom_row() );
|
||||
} else if ( ds.get_cursor_row() + rows < ds.get_scrolling_region_top_row() ) {
|
||||
scroll( ds.get_cursor_row() + rows - ds.get_scrolling_region_top_row() );
|
||||
}
|
||||
|
||||
ds.move_row( rows, true );
|
||||
}
|
||||
|
||||
Cell *Framebuffer::get_combining_cell( void )
|
||||
{
|
||||
if ( (ds.get_combining_char_col() < 0)
|
||||
|| (ds.get_combining_char_row() < 0)
|
||||
|| (ds.get_combining_char_col() >= ds.get_width())
|
||||
|| (ds.get_combining_char_row() >= ds.get_height()) ) {
|
||||
return NULL;
|
||||
} /* can happen if a resize came in between */
|
||||
|
||||
return &rows[ ds.get_combining_char_row() ].cells[ ds.get_combining_char_col() ];
|
||||
}
|
||||
|
||||
void DrawState::set_tab( void )
|
||||
{
|
||||
tabs[ cursor_col ] = true;
|
||||
}
|
||||
|
||||
void DrawState::clear_tab( int col )
|
||||
{
|
||||
tabs[ col ] = false;
|
||||
}
|
||||
|
||||
int DrawState::get_next_tab( void )
|
||||
{
|
||||
for ( int i = cursor_col + 1; i < width; i++ ) {
|
||||
if ( tabs[ i ] ) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void DrawState::set_scrolling_region( int top, int bottom )
|
||||
{
|
||||
if ( height < 1 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrolling_region_top_row = top;
|
||||
scrolling_region_bottom_row = bottom;
|
||||
|
||||
if ( scrolling_region_top_row < 0 ) scrolling_region_top_row = 0;
|
||||
if ( scrolling_region_bottom_row >= height ) scrolling_region_bottom_row = height - 1;
|
||||
|
||||
if ( scrolling_region_bottom_row < scrolling_region_top_row )
|
||||
scrolling_region_bottom_row = scrolling_region_top_row;
|
||||
/* real rule requires TWO-line scrolling region */
|
||||
|
||||
if ( origin_mode ) {
|
||||
snap_cursor_to_border();
|
||||
new_grapheme();
|
||||
}
|
||||
}
|
||||
|
||||
int DrawState::limit_top( void )
|
||||
{
|
||||
return origin_mode ? scrolling_region_top_row : 0;
|
||||
}
|
||||
|
||||
int DrawState::limit_bottom( void )
|
||||
{
|
||||
return origin_mode ? scrolling_region_bottom_row : height - 1;
|
||||
}
|
||||
|
||||
std::vector<int> DrawState::get_tabs( void )
|
||||
{
|
||||
std::vector<int> ret;
|
||||
|
||||
for ( int i = 0; i < width; i++ ) {
|
||||
if ( tabs[ i ] ) {
|
||||
ret.push_back( i );
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void Framebuffer::apply_renditions_to_current_cell( void )
|
||||
{
|
||||
get_mutable_cell()->renditions = ds.get_renditions();
|
||||
}
|
||||
|
||||
SavedCursor::SavedCursor()
|
||||
: cursor_col( 0 ), cursor_row( 0 ),
|
||||
renditions( 0 ),
|
||||
auto_wrap_mode( true ),
|
||||
origin_mode( false )
|
||||
{}
|
||||
|
||||
void DrawState::save_cursor( void )
|
||||
{
|
||||
save.cursor_col = cursor_col;
|
||||
save.cursor_row = cursor_row;
|
||||
save.renditions = renditions;
|
||||
save.auto_wrap_mode = auto_wrap_mode;
|
||||
save.origin_mode = origin_mode;
|
||||
}
|
||||
|
||||
void DrawState::restore_cursor( void )
|
||||
{
|
||||
cursor_col = save.cursor_col;
|
||||
cursor_row = save.cursor_row;
|
||||
renditions = save.renditions;
|
||||
auto_wrap_mode = save.auto_wrap_mode;
|
||||
origin_mode = save.origin_mode;
|
||||
|
||||
snap_cursor_to_border(); /* we could have resized in between */
|
||||
new_grapheme();
|
||||
}
|
||||
|
||||
void Framebuffer::insert_line( int before_row )
|
||||
{
|
||||
if ( (before_row < ds.get_scrolling_region_top_row())
|
||||
|| (before_row > ds.get_scrolling_region_bottom_row() + 1) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
rows.insert( rows.begin() + before_row, newrow() );
|
||||
rows.erase( rows.begin() + ds.get_scrolling_region_bottom_row() + 1 );
|
||||
}
|
||||
|
||||
void Framebuffer::delete_line( int row )
|
||||
{
|
||||
if ( (row < ds.get_scrolling_region_top_row())
|
||||
|| (row > ds.get_scrolling_region_bottom_row()) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
int insertbefore = ds.get_scrolling_region_bottom_row() + 1;
|
||||
if ( insertbefore == ds.get_height() ) {
|
||||
rows.push_back( newrow() );
|
||||
} else {
|
||||
rows.insert( rows.begin() + insertbefore, newrow() );
|
||||
}
|
||||
|
||||
rows.erase( rows.begin() + row );
|
||||
}
|
||||
|
||||
void Row::insert_cell( int col, int background_color )
|
||||
{
|
||||
cells.insert( cells.begin() + col, Cell( background_color ) );
|
||||
cells.pop_back();
|
||||
}
|
||||
|
||||
void Row::delete_cell( int col, int background_color )
|
||||
{
|
||||
cells.push_back( Cell( background_color ) );
|
||||
cells.erase( cells.begin() + col );
|
||||
}
|
||||
|
||||
void Framebuffer::insert_cell( int row, int col )
|
||||
{
|
||||
rows[ row ].insert_cell( col, ds.get_background_rendition() );
|
||||
}
|
||||
|
||||
void Framebuffer::delete_cell( int row, int col )
|
||||
{
|
||||
rows[ row ].delete_cell( col, ds.get_background_rendition() );
|
||||
}
|
||||
|
||||
void Framebuffer::reset( void )
|
||||
{
|
||||
int width = ds.get_width(), height = ds.get_height();
|
||||
ds = DrawState( width, height );
|
||||
rows = std::deque<Row>( height, newrow() );
|
||||
window_title.clear();
|
||||
/* do not reset bell_count */
|
||||
}
|
||||
|
||||
void Framebuffer::soft_reset( void )
|
||||
{
|
||||
ds.insert_mode = false;
|
||||
ds.origin_mode = false;
|
||||
ds.cursor_visible = true; /* per xterm and gnome-terminal */
|
||||
ds.application_mode_cursor_keys = false;
|
||||
ds.set_scrolling_region( 0, ds.get_height() - 1 );
|
||||
ds.add_rendition( 0 );
|
||||
ds.clear_saved_cursor();
|
||||
}
|
||||
|
||||
void Framebuffer::resize( int s_width, int s_height )
|
||||
{
|
||||
assert( s_width > 0 );
|
||||
assert( s_height > 0 );
|
||||
|
||||
rows.resize( s_height, newrow() );
|
||||
|
||||
for ( std::deque<Row>::iterator i = rows.begin();
|
||||
i != rows.end();
|
||||
i++ ) {
|
||||
(*i).cells.resize( s_width, Cell( ds.get_background_rendition() ) );
|
||||
}
|
||||
|
||||
ds.resize( s_width, s_height );
|
||||
}
|
||||
|
||||
void DrawState::resize( int s_width, int s_height )
|
||||
{
|
||||
if ( (width != s_width)
|
||||
|| (height != s_height) ) {
|
||||
/* reset entire scrolling region on any resize */
|
||||
/* xterm and rxvt-unicode do this. gnome-terminal only
|
||||
resets scrolling region if it has to become smaller in resize */
|
||||
scrolling_region_top_row = 0;
|
||||
scrolling_region_bottom_row = s_height - 1;
|
||||
}
|
||||
|
||||
width = s_width;
|
||||
height = s_height;
|
||||
|
||||
snap_cursor_to_border();
|
||||
|
||||
tabs.resize( width );
|
||||
|
||||
/* saved cursor will be snapped to border on restore */
|
||||
|
||||
/* invalidate combining char cell if necessary */
|
||||
if ( (combining_char_col >= width)
|
||||
|| (combining_char_row >= height) ) {
|
||||
combining_char_col = combining_char_row = -1;
|
||||
}
|
||||
}
|
||||
|
||||
Renditions::Renditions( int s_background )
|
||||
: bold( false ), underlined( false ), blink( false ),
|
||||
inverse( false ), invisible( false ), foreground_color( 0 ),
|
||||
background_color( s_background )
|
||||
{}
|
||||
|
||||
void Renditions::set_rendition( int num )
|
||||
{
|
||||
if ( num == 0 ) {
|
||||
bold = underlined = blink = inverse = invisible = false;
|
||||
foreground_color = background_color = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if ( (30 <= num) && (num <= 39) ) { /* foreground color */
|
||||
foreground_color = num;
|
||||
return;
|
||||
} else if ( (40 <= num) && (num <= 49) ) { /* background color */
|
||||
background_color = num;
|
||||
return;
|
||||
}
|
||||
|
||||
switch ( num ) {
|
||||
case 1: case 22: bold = (num == 1); break;
|
||||
case 4: case 24: underlined = (num == 4); break;
|
||||
case 5: case 25: blink = (num == 5); break;
|
||||
case 7: case 27: inverse = (num == 7); break;
|
||||
case 8: case 28: invisible = (num == 8); break;
|
||||
}
|
||||
}
|
||||
|
||||
std::string Renditions::sgr( void ) const
|
||||
{
|
||||
std::string ret;
|
||||
|
||||
ret.append( "\033[0" );
|
||||
if ( bold ) ret.append( ";1" );
|
||||
if ( underlined ) ret.append( ";4" );
|
||||
if ( blink ) ret.append( ";5" );
|
||||
if ( inverse ) ret.append( ";7" );
|
||||
if ( invisible ) ret.append( ";8" );
|
||||
if ( foreground_color ) {
|
||||
char col[ 8 ];
|
||||
snprintf( col, 8, ";%d", foreground_color );
|
||||
ret.append( col );
|
||||
}
|
||||
if ( background_color ) {
|
||||
char col[ 8 ];
|
||||
snprintf( col, 8, ";%d", background_color );
|
||||
ret.append( col );
|
||||
}
|
||||
ret.append( "m" );
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void Row::reset( int background_color )
|
||||
{
|
||||
for ( std::vector<Cell>::iterator i = cells.begin();
|
||||
i != cells.end();
|
||||
i++ ) {
|
||||
i->reset( background_color );
|
||||
}
|
||||
}
|
||||
|
||||
void Framebuffer::prefix_window_title( const std::deque<wchar_t> &s )
|
||||
{
|
||||
for ( auto i = s.rbegin(); i != s.rend(); i++ ) {
|
||||
window_title.push_front( *i );
|
||||
}
|
||||
}
|
||||
|
||||
wchar_t Cell::debug_contents( void ) const
|
||||
{
|
||||
if ( contents.empty() ) {
|
||||
return '_';
|
||||
} else {
|
||||
return contents.front();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
#ifndef TERMINALFB_HPP
|
||||
#define TERMINALFB_HPP
|
||||
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
#include <string>
|
||||
#include <list>
|
||||
#include <assert.h>
|
||||
|
||||
/* Terminal framebuffer */
|
||||
|
||||
namespace Terminal {
|
||||
class Renditions {
|
||||
public:
|
||||
bool bold, underlined, blink, inverse, invisible;
|
||||
int foreground_color;
|
||||
int background_color;
|
||||
|
||||
Renditions( int s_background );
|
||||
void set_rendition( int num );
|
||||
std::string sgr( void ) const;
|
||||
|
||||
bool operator==( const Renditions &x ) const
|
||||
{
|
||||
return (bold == x.bold) && (underlined == x.underlined)
|
||||
&& (blink == x.blink) && (inverse == x.inverse)
|
||||
&& (invisible == x.invisible) && (foreground_color == x.foreground_color)
|
||||
&& (background_color == x.background_color);
|
||||
}
|
||||
};
|
||||
|
||||
class Cell {
|
||||
public:
|
||||
std::vector<wchar_t> contents;
|
||||
char fallback; /* first character is combining character */
|
||||
int width;
|
||||
Renditions renditions;
|
||||
|
||||
Cell( int background_color )
|
||||
: contents(),
|
||||
fallback( false ),
|
||||
width( 1 ),
|
||||
renditions( background_color )
|
||||
{}
|
||||
|
||||
Cell() /* default constructor required by C++11 STL */
|
||||
: contents(),
|
||||
fallback( false ),
|
||||
width( 1 ),
|
||||
renditions( 0 )
|
||||
{
|
||||
assert( false );
|
||||
}
|
||||
|
||||
void reset( int background_color );
|
||||
|
||||
bool operator==( const Cell &x ) const
|
||||
{
|
||||
return ( (contents == x.contents)
|
||||
&& (fallback == x.fallback)
|
||||
&& (width == x.width)
|
||||
&& (renditions == x.renditions) );
|
||||
}
|
||||
|
||||
wchar_t debug_contents( void ) const;
|
||||
|
||||
bool is_blank( void ) const
|
||||
{
|
||||
return ( contents.empty()
|
||||
|| ( (contents.size() == 1) && ( (contents.front() == 0x20)
|
||||
|| (contents.front() == 0xA0) ) ) );
|
||||
}
|
||||
};
|
||||
|
||||
class Row {
|
||||
public:
|
||||
std::vector<Cell> cells;
|
||||
bool wrap;
|
||||
|
||||
Row( size_t s_width, int background_color )
|
||||
: cells( s_width, Cell( background_color ) ), wrap( false )
|
||||
{}
|
||||
|
||||
Row() /* default constructor required by C++11 STL */
|
||||
: cells( 1, Cell() ), wrap( false )
|
||||
{
|
||||
assert( false );
|
||||
}
|
||||
|
||||
void insert_cell( int col, int background_color );
|
||||
void delete_cell( int col, int background_color );
|
||||
|
||||
void reset( int background_color );
|
||||
|
||||
bool operator==( const Row &x ) const
|
||||
{
|
||||
return ( (cells == x.cells) && (wrap == x.wrap) );
|
||||
}
|
||||
};
|
||||
|
||||
class SavedCursor {
|
||||
public:
|
||||
int cursor_col, cursor_row;
|
||||
Renditions renditions;
|
||||
/* not implemented: character set shift state */
|
||||
bool auto_wrap_mode;
|
||||
bool origin_mode;
|
||||
/* not implemented: state of selective erase */
|
||||
|
||||
SavedCursor();
|
||||
};
|
||||
|
||||
class DrawState {
|
||||
private:
|
||||
int width, height;
|
||||
|
||||
void new_grapheme( void );
|
||||
void snap_cursor_to_border( void );
|
||||
|
||||
int cursor_col, cursor_row;
|
||||
int combining_char_col, combining_char_row;
|
||||
|
||||
std::vector<bool> tabs;
|
||||
|
||||
int scrolling_region_top_row, scrolling_region_bottom_row;
|
||||
|
||||
Renditions renditions;
|
||||
|
||||
SavedCursor save;
|
||||
|
||||
public:
|
||||
bool next_print_will_wrap;
|
||||
bool origin_mode;
|
||||
bool auto_wrap_mode;
|
||||
bool insert_mode;
|
||||
bool cursor_visible;
|
||||
bool reverse_video;
|
||||
|
||||
bool application_mode_cursor_keys;
|
||||
|
||||
/* bold, etc. */
|
||||
|
||||
void move_row( int N, bool relative = false );
|
||||
void move_col( int N, bool relative = false, bool implicit = false );
|
||||
|
||||
int get_cursor_col( void ) const { return cursor_col; }
|
||||
int get_cursor_row( void ) const { return cursor_row; }
|
||||
int get_combining_char_col( void ) const { return combining_char_col; }
|
||||
int get_combining_char_row( void ) const { return combining_char_row; }
|
||||
int get_width( void ) const { return width; }
|
||||
int get_height( void ) const { return height; }
|
||||
|
||||
void set_tab( void );
|
||||
void clear_tab( int col );
|
||||
int get_next_tab( void );
|
||||
|
||||
std::vector<int> get_tabs( void );
|
||||
|
||||
void set_scrolling_region( int top, int bottom );
|
||||
|
||||
int get_scrolling_region_top_row( void ) const { return scrolling_region_top_row; }
|
||||
int get_scrolling_region_bottom_row( void ) const { return scrolling_region_bottom_row; }
|
||||
|
||||
int limit_top( void );
|
||||
int limit_bottom( void );
|
||||
|
||||
void add_rendition( int x ) { renditions.set_rendition( x ); }
|
||||
Renditions get_renditions( void ) const { return renditions; }
|
||||
int get_background_rendition( void ) { return renditions.background_color; }
|
||||
|
||||
void save_cursor( void );
|
||||
void restore_cursor( void );
|
||||
void clear_saved_cursor( void ) { save = SavedCursor(); }
|
||||
|
||||
void resize( int s_width, int s_height );
|
||||
|
||||
DrawState( int s_width, int s_height );
|
||||
|
||||
bool operator==( const DrawState &x ) const
|
||||
{
|
||||
/* only compare fields that affect display */
|
||||
return ( width == x.width ) && ( height == x.height ) && ( cursor_col == x.cursor_col )
|
||||
&& ( cursor_row == x.cursor_row ) && ( cursor_visible == x.cursor_visible ) &&
|
||||
( reverse_video == x.reverse_video ) && ( renditions == x.renditions );
|
||||
}
|
||||
};
|
||||
|
||||
class Framebuffer {
|
||||
private:
|
||||
std::deque<Row> rows;
|
||||
std::deque<wchar_t> window_title;
|
||||
unsigned int bell_count;
|
||||
|
||||
Row newrow( void ) { return Row( ds.get_width(), ds.get_background_rendition() ); }
|
||||
|
||||
public:
|
||||
Framebuffer( int s_width, int s_height );
|
||||
DrawState ds;
|
||||
|
||||
void scroll( int N );
|
||||
void move_rows_autoscroll( int rows );
|
||||
|
||||
const Row *get_row( int row ) const
|
||||
{
|
||||
if ( row == -1 ) row = ds.get_cursor_row();
|
||||
|
||||
return &rows[ row ];
|
||||
}
|
||||
|
||||
inline const Cell *get_cell( void ) const
|
||||
{
|
||||
return &rows[ ds.get_cursor_row() ].cells[ ds.get_cursor_col() ];
|
||||
}
|
||||
|
||||
inline const Cell *get_cell( int row, int col ) const
|
||||
{
|
||||
if ( row == -1 ) row = ds.get_cursor_row();
|
||||
if ( col == -1 ) col = ds.get_cursor_col();
|
||||
|
||||
return &rows[ row ].cells[ col ];
|
||||
}
|
||||
|
||||
Row *get_mutable_row( int row )
|
||||
{
|
||||
if ( row == -1 ) row = ds.get_cursor_row();
|
||||
|
||||
return &rows[ row ];
|
||||
}
|
||||
|
||||
inline Cell *get_mutable_cell( void )
|
||||
{
|
||||
return &rows[ ds.get_cursor_row() ].cells[ ds.get_cursor_col() ];
|
||||
}
|
||||
|
||||
inline Cell *get_mutable_cell( int row, int col )
|
||||
{
|
||||
if ( row == -1 ) row = ds.get_cursor_row();
|
||||
if ( col == -1 ) col = ds.get_cursor_col();
|
||||
|
||||
return &rows[ row ].cells[ col ];
|
||||
}
|
||||
|
||||
Cell *get_combining_cell( void );
|
||||
|
||||
void apply_renditions_to_current_cell( void );
|
||||
|
||||
void insert_line( int before_row );
|
||||
void delete_line( int row );
|
||||
|
||||
void insert_cell( int row, int col );
|
||||
void delete_cell( int row, int col );
|
||||
|
||||
void reset( void );
|
||||
void soft_reset( void );
|
||||
|
||||
void set_window_title( const std::deque<wchar_t> &s ) { window_title = s; }
|
||||
const std::deque<wchar_t> & get_window_title( void ) const { return window_title; }
|
||||
|
||||
void prefix_window_title( const std::deque<wchar_t> &s );
|
||||
|
||||
void resize( int s_width, int s_height );
|
||||
|
||||
void reset_cell( Cell *c ) { c->reset( ds.get_background_rendition() ); }
|
||||
void reset_row( Row *r ) { r->reset( ds.get_background_rendition() ); }
|
||||
|
||||
void ring_bell( void ) { bell_count++; }
|
||||
unsigned int get_bell_count( void ) const { return bell_count; }
|
||||
|
||||
bool operator==( const Framebuffer &x ) const
|
||||
{
|
||||
return ( rows == x.rows ) && ( window_title == x.window_title ) && ( bell_count == x.bell_count ) && ( ds == x.ds );
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,493 @@
|
||||
#include <unistd.h>
|
||||
#include <string>
|
||||
|
||||
#include "terminaldispatcher.hpp"
|
||||
#include "terminalframebuffer.hpp"
|
||||
#include "parseraction.hpp"
|
||||
|
||||
using namespace Terminal;
|
||||
|
||||
/* Terminal functions -- routines activated by CSI, escape or a control char */
|
||||
|
||||
static void clearline( Framebuffer *fb, int row, int start, int end )
|
||||
{
|
||||
for ( int col = start; col <= end; col++ ) {
|
||||
fb->reset_cell( fb->get_mutable_cell( row, col ) );
|
||||
}
|
||||
}
|
||||
|
||||
/* erase in line */
|
||||
void CSI_EL( Framebuffer *fb, Dispatcher *dispatch )
|
||||
{
|
||||
switch ( dispatch->getparam( 0, 0 ) ) {
|
||||
case 0: /* default: active position to end of line, inclusive */
|
||||
clearline( fb, -1, fb->ds.get_cursor_col(), fb->ds.get_width() - 1 );
|
||||
break;
|
||||
case 1: /* start of screen to active position, inclusive */
|
||||
clearline( fb, -1, 0, fb->ds.get_cursor_col() );
|
||||
break;
|
||||
case 2: /* all of line */
|
||||
fb->reset_row( fb->get_mutable_row( -1 ) );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static Function func_CSI_EL( CSI, "K", CSI_EL );
|
||||
|
||||
/* erase in display */
|
||||
void CSI_ED( Framebuffer *fb, Dispatcher *dispatch ) {
|
||||
switch ( dispatch->getparam( 0, 0 ) ) {
|
||||
case 0: /* active position to end of screen, inclusive */
|
||||
clearline( fb, -1, fb->ds.get_cursor_col(), fb->ds.get_width() - 1 );
|
||||
for ( int y = fb->ds.get_cursor_row() + 1; y < fb->ds.get_height(); y++ ) {
|
||||
fb->reset_row( fb->get_mutable_row( y ) );
|
||||
}
|
||||
break;
|
||||
case 1: /* start of screen to active position, inclusive */
|
||||
for ( int y = 0; y < fb->ds.get_cursor_row(); y++ ) {
|
||||
fb->reset_row( fb->get_mutable_row( y ) );
|
||||
}
|
||||
clearline( fb, -1, 0, fb->ds.get_cursor_col() );
|
||||
break;
|
||||
case 2: /* entire screen */
|
||||
for ( int y = 0; y < fb->ds.get_height(); y++ ) {
|
||||
fb->reset_row( fb->get_mutable_row( y ) );
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static Function func_CSI_ED( CSI, "J", CSI_ED );
|
||||
|
||||
/* cursor movement -- relative and absolute */
|
||||
void CSI_cursormove( Framebuffer *fb, Dispatcher *dispatch )
|
||||
{
|
||||
int num = dispatch->getparam( 0, 1 );
|
||||
|
||||
switch ( dispatch->get_dispatch_chars()[ 0 ] ) {
|
||||
case 'A':
|
||||
fb->ds.move_row( -num, true );
|
||||
break;
|
||||
case 'B':
|
||||
fb->ds.move_row( num, true );
|
||||
break;
|
||||
case 'C':
|
||||
fb->ds.move_col( num, true );
|
||||
break;
|
||||
case 'D':
|
||||
fb->ds.move_col( -num, true );
|
||||
break;
|
||||
case 'H':
|
||||
case 'f':
|
||||
int x = dispatch->getparam( 0, 1 );
|
||||
int y = dispatch->getparam( 1, 1 );
|
||||
fb->ds.move_row( x - 1 );
|
||||
fb->ds.move_col( y - 1 );
|
||||
}
|
||||
}
|
||||
|
||||
static Function func_CSI_cursormove_A( CSI, "A", CSI_cursormove );
|
||||
static Function func_CSI_cursormove_B( CSI, "B", CSI_cursormove );
|
||||
static Function func_CSI_cursormove_C( CSI, "C", CSI_cursormove );
|
||||
static Function func_CSI_cursormove_D( CSI, "D", CSI_cursormove );
|
||||
static Function func_CSI_cursormove_H( CSI, "H", CSI_cursormove );
|
||||
static Function func_CSI_cursormove_f( CSI, "f", CSI_cursormove );
|
||||
|
||||
/* device attributes */
|
||||
void CSI_DA( Framebuffer *fb __attribute((unused)), Dispatcher *dispatch )
|
||||
{
|
||||
dispatch->terminal_to_host.append( "\033[?62c" ); /* plain vt220 */
|
||||
}
|
||||
|
||||
static Function func_CSI_DA( CSI, "c", CSI_DA );
|
||||
|
||||
/* secondary device attributes */
|
||||
void CSI_SDA( Framebuffer *fb __attribute((unused)), Dispatcher *dispatch )
|
||||
{
|
||||
dispatch->terminal_to_host.append( "\033[>1;10;0c" ); /* plain vt220 */
|
||||
}
|
||||
|
||||
static Function func_CSI_SDA( CSI, ">c", CSI_SDA );
|
||||
|
||||
/* screen alignment diagnostic */
|
||||
void Esc_DECALN( Framebuffer *fb, Dispatcher *dispatch __attribute((unused)) )
|
||||
{
|
||||
for ( int y = 0; y < fb->ds.get_height(); y++ ) {
|
||||
for ( int x = 0; x < fb->ds.get_width(); x++ ) {
|
||||
fb->reset_cell( fb->get_mutable_cell( y, x ) );
|
||||
fb->get_mutable_cell( y, x )->contents.push_back( L'E' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Function func_Esc_DECALN( ESCAPE, "#8", Esc_DECALN );
|
||||
|
||||
/* line feed */
|
||||
void Ctrl_LF( Framebuffer *fb, Dispatcher *dispatch __attribute((unused)) )
|
||||
{
|
||||
fb->move_rows_autoscroll( 1 );
|
||||
}
|
||||
|
||||
/* same procedure for index, vertical tab, and form feed control codes */
|
||||
static Function func_Ctrl_LF( CONTROL, "\x0a", Ctrl_LF );
|
||||
static Function func_Ctrl_IND( CONTROL, "\x84", Ctrl_LF );
|
||||
static Function func_Ctrl_VT( CONTROL, "\x0b", Ctrl_LF );
|
||||
static Function func_Ctrl_FF( CONTROL, "\x0c", Ctrl_LF );
|
||||
|
||||
/* carriage return */
|
||||
void Ctrl_CR( Framebuffer *fb, Dispatcher *dispatch __attribute((unused)) )
|
||||
{
|
||||
fb->ds.move_col( 0 );
|
||||
}
|
||||
|
||||
static Function func_Ctrl_CR( CONTROL, "\x0d", Ctrl_CR );
|
||||
|
||||
/* backspace */
|
||||
void Ctrl_BS( Framebuffer *fb, Dispatcher *dispatch __attribute((unused)) )
|
||||
{
|
||||
fb->ds.move_col( -1, true );
|
||||
}
|
||||
|
||||
static Function func_Ctrl_BS( CONTROL, "\x08", Ctrl_BS );
|
||||
|
||||
/* reverse index -- like a backwards line feed */
|
||||
void Ctrl_RI( Framebuffer *fb, Dispatcher *dispatch __attribute((unused)) )
|
||||
{
|
||||
fb->move_rows_autoscroll( -1 );
|
||||
}
|
||||
|
||||
static Function func_Ctrl_RI( CONTROL, "\x8D", Ctrl_RI );
|
||||
|
||||
/* newline */
|
||||
void Ctrl_NEL( Framebuffer *fb, Dispatcher *dispatch __attribute((unused)) )
|
||||
{
|
||||
fb->ds.move_col( 0 );
|
||||
fb->move_rows_autoscroll( 1 );
|
||||
}
|
||||
|
||||
static Function func_Ctrl_NEL( CONTROL, "\x85", Ctrl_NEL );
|
||||
|
||||
/* horizontal tab */
|
||||
void Ctrl_HT( Framebuffer *fb, Dispatcher *dispatch __attribute((unused)) )
|
||||
{
|
||||
int col = fb->ds.get_next_tab();
|
||||
if ( col == -1 ) { /* no tabs, go to end of line */
|
||||
fb->ds.move_col( fb->ds.get_width() - 1 );
|
||||
} else {
|
||||
fb->ds.move_col( col );
|
||||
}
|
||||
}
|
||||
|
||||
static Function func_Ctrl_HT( CONTROL, "\x09", Ctrl_HT );
|
||||
|
||||
/* horizontal tab set */
|
||||
void Ctrl_HTS( Framebuffer *fb, Dispatcher *dispatch __attribute((unused)) )
|
||||
{
|
||||
fb->ds.set_tab();
|
||||
}
|
||||
|
||||
static Function func_Ctrl_HTS( CONTROL, "\x88", Ctrl_HTS );
|
||||
|
||||
/* tabulation clear */
|
||||
void CSI_TBC( Framebuffer *fb, Dispatcher *dispatch )
|
||||
{
|
||||
int param = dispatch->getparam( 0, 0 );
|
||||
switch ( param ) {
|
||||
case 0: /* clear this tab stop */
|
||||
fb->ds.clear_tab( fb->ds.get_cursor_col() );
|
||||
break;
|
||||
case 3: /* clear all tab stops */
|
||||
for ( int x = 0; x < fb->ds.get_width(); x++ ) {
|
||||
fb->ds.clear_tab( x );
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static Function func_CSI_TBC( CSI, "g", CSI_TBC );
|
||||
|
||||
static bool *get_DEC_mode( int param, Framebuffer *fb ) {
|
||||
switch ( param ) {
|
||||
case 1: /* cursor key mode */
|
||||
return &(fb->ds.application_mode_cursor_keys);
|
||||
case 3: /* 80/132. Ignore but clear screen. */
|
||||
/* clear screen */
|
||||
fb->ds.move_row( 0 );
|
||||
fb->ds.move_col( 0 );
|
||||
for ( int y = 0; y < fb->ds.get_height(); y++ ) {
|
||||
fb->reset_row( fb->get_mutable_row( y ) );
|
||||
}
|
||||
return NULL;
|
||||
case 5: /* reverse video */
|
||||
return &(fb->ds.reverse_video);
|
||||
case 6: /* origin */
|
||||
fb->ds.move_row( 0 );
|
||||
fb->ds.move_col( 0 );
|
||||
return &(fb->ds.origin_mode);
|
||||
case 7: /* auto wrap */
|
||||
return &(fb->ds.auto_wrap_mode);
|
||||
case 25:
|
||||
return &(fb->ds.cursor_visible);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* set private mode */
|
||||
void CSI_DECSM( Framebuffer *fb, Dispatcher *dispatch )
|
||||
{
|
||||
for ( int i = 0; i < dispatch->param_count(); i++ ) {
|
||||
bool *mode = get_DEC_mode( dispatch->getparam( i, 0 ), fb );
|
||||
if ( mode ) {
|
||||
*mode = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* clear private mode */
|
||||
void CSI_DECRM( Framebuffer *fb, Dispatcher *dispatch )
|
||||
{
|
||||
for ( int i = 0; i < dispatch->param_count(); i++ ) {
|
||||
bool *mode = get_DEC_mode( dispatch->getparam( i, 0 ), fb );
|
||||
if ( mode ) {
|
||||
*mode = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Function func_CSI_DECSM( CSI, "?h", CSI_DECSM );
|
||||
static Function func_CSI_DECRM( CSI, "?l", CSI_DECRM );
|
||||
|
||||
static bool *get_ANSI_mode( int param, Framebuffer *fb ) {
|
||||
switch ( param ) {
|
||||
case 4: /* insert/replace mode */
|
||||
return &(fb->ds.insert_mode);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* set mode */
|
||||
void CSI_SM( Framebuffer *fb, Dispatcher *dispatch )
|
||||
{
|
||||
for ( int i = 0; i < dispatch->param_count(); i++ ) {
|
||||
bool *mode = get_ANSI_mode( dispatch->getparam( i, 0 ), fb );
|
||||
if ( mode ) {
|
||||
*mode = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* clear mode */
|
||||
void CSI_RM( Framebuffer *fb, Dispatcher *dispatch )
|
||||
{
|
||||
for ( int i = 0; i < dispatch->param_count(); i++ ) {
|
||||
bool *mode = get_ANSI_mode( dispatch->getparam( i, 0 ), fb );
|
||||
if ( mode ) {
|
||||
*mode = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Function func_CSI_SM( CSI, "h", CSI_SM );
|
||||
static Function func_CSI_RM( CSI, "l", CSI_RM );
|
||||
|
||||
/* set top and bottom margins */
|
||||
void CSI_DECSTBM( Framebuffer *fb, Dispatcher *dispatch )
|
||||
{
|
||||
int top = dispatch->getparam( 0, 1 );
|
||||
int bottom = dispatch->getparam( 1, fb->ds.get_height() );
|
||||
|
||||
fb->ds.set_scrolling_region( top - 1, bottom - 1 );
|
||||
fb->ds.move_row( 0 );
|
||||
fb->ds.move_col( 0 );
|
||||
}
|
||||
|
||||
static Function func_CSI_DECSTMB( CSI, "r", CSI_DECSTBM );
|
||||
|
||||
/* terminal bell */
|
||||
void Ctrl_BEL( Framebuffer *fb, Dispatcher *dispatch __attribute((unused)) ) {
|
||||
fb->ring_bell();
|
||||
}
|
||||
|
||||
static Function func_Ctrl_BEL( CONTROL, "\x07", Ctrl_BEL );
|
||||
|
||||
/* select graphics rendition -- e.g., bold, blinking, etc. */
|
||||
void CSI_SGR( Framebuffer *fb, Dispatcher *dispatch )
|
||||
{
|
||||
for ( int i = 0; i < dispatch->param_count(); i++ ) {
|
||||
int rendition = dispatch->getparam( i, 0 );
|
||||
fb->ds.add_rendition( rendition );
|
||||
}
|
||||
}
|
||||
|
||||
static Function func_CSI_SGR( CSI, "m", CSI_SGR, false ); /* changing renditions doesn't clear wrap flag */
|
||||
|
||||
/* save and restore cursor */
|
||||
void Esc_DECSC( Framebuffer *fb, Dispatcher *dispatch __attribute((unused)) )
|
||||
{
|
||||
fb->ds.save_cursor();
|
||||
}
|
||||
|
||||
void Esc_DECRC( Framebuffer *fb, Dispatcher *dispatch __attribute((unused)) )
|
||||
{
|
||||
fb->ds.restore_cursor();
|
||||
}
|
||||
|
||||
static Function func_Esc_DECSC( ESCAPE, "7", Esc_DECSC );
|
||||
static Function func_Esc_DECRC( ESCAPE, "8", Esc_DECRC );
|
||||
|
||||
/* device status report -- e.g., cursor position (used by resize) */
|
||||
void CSI_DSR( Framebuffer *fb, Dispatcher *dispatch )
|
||||
{
|
||||
int param = dispatch->getparam( 0, 0 );
|
||||
|
||||
switch ( param ) {
|
||||
case 5: /* device status report requested */
|
||||
dispatch->terminal_to_host.append( "\033[0n" );
|
||||
break;
|
||||
case 6: /* report of active position requested */
|
||||
char cpr[ 32 ];
|
||||
snprintf( cpr, 32, "\033[%d;%dR",
|
||||
fb->ds.get_cursor_row() + 1,
|
||||
fb->ds.get_cursor_col() + 1 );
|
||||
dispatch->terminal_to_host.append( cpr );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static Function func_CSI_DSR( CSI, "n", CSI_DSR );
|
||||
|
||||
/* insert line */
|
||||
void CSI_IL( Framebuffer *fb, Dispatcher *dispatch )
|
||||
{
|
||||
int lines = dispatch->getparam( 0, 1 );
|
||||
|
||||
for ( int i = 0; i < lines; i++ ) {
|
||||
fb->insert_line( fb->ds.get_cursor_row() );
|
||||
}
|
||||
|
||||
/* vt220 manual and Ecma-48 say to move to first column */
|
||||
/* but xterm and gnome-terminal don't */
|
||||
fb->ds.move_col( 0 );
|
||||
}
|
||||
|
||||
static Function func_CSI_IL( CSI, "L", CSI_IL );
|
||||
|
||||
/* delete line */
|
||||
void CSI_DL( Framebuffer *fb, Dispatcher *dispatch )
|
||||
{
|
||||
int lines = dispatch->getparam( 0, 1 );
|
||||
|
||||
for ( int i = 0; i < lines; i++ ) {
|
||||
fb->delete_line( fb->ds.get_cursor_row() );
|
||||
}
|
||||
|
||||
/* same story -- xterm and gnome-terminal don't
|
||||
move to first column */
|
||||
fb->ds.move_col( 0 );
|
||||
}
|
||||
|
||||
static Function func_CSI_DL( CSI, "M", CSI_DL );
|
||||
|
||||
/* insert characters */
|
||||
void CSI_ICH( Framebuffer *fb, Dispatcher *dispatch )
|
||||
{
|
||||
int cells = dispatch->getparam( 0, 1 );
|
||||
|
||||
for ( int i = 0; i < cells; i++ ) {
|
||||
fb->insert_cell( fb->ds.get_cursor_row(), fb->ds.get_cursor_col() );
|
||||
}
|
||||
}
|
||||
|
||||
static Function func_CSI_ICH( CSI, "@", CSI_ICH );
|
||||
|
||||
/* delete character */
|
||||
void CSI_DCH( Framebuffer *fb, Dispatcher *dispatch )
|
||||
{
|
||||
int cells = dispatch->getparam( 0, 1 );
|
||||
|
||||
for ( int i = 0; i < cells; i++ ) {
|
||||
fb->delete_cell( fb->ds.get_cursor_row(), fb->ds.get_cursor_col() );
|
||||
}
|
||||
}
|
||||
|
||||
static Function func_CSI_DCH( CSI, "P", CSI_DCH );
|
||||
|
||||
/* line position absolute */
|
||||
void CSI_VPA( Framebuffer *fb, Dispatcher *dispatch )
|
||||
{
|
||||
int row = dispatch->getparam( 0, 1 );
|
||||
fb->ds.move_row( row - 1 );
|
||||
}
|
||||
|
||||
static Function func_CSI_VPA( CSI, "d", CSI_VPA );
|
||||
|
||||
/* character position absolute */
|
||||
void CSI_HPA( Framebuffer *fb, Dispatcher *dispatch )
|
||||
{
|
||||
int col = dispatch->getparam( 0, 1 );
|
||||
fb->ds.move_col( col - 1 );
|
||||
}
|
||||
|
||||
static Function func_CSI_CHA( CSI, "G", CSI_HPA ); /* ECMA-48 name: CHA */
|
||||
static Function func_CSI_HPA( CSI, "\x60", CSI_HPA ); /* ECMA-48 name: HPA */
|
||||
|
||||
/* erase character */
|
||||
void CSI_ECH( Framebuffer *fb, Dispatcher *dispatch )
|
||||
{
|
||||
int num = dispatch->getparam( 0, 1 );
|
||||
int limit = fb->ds.get_cursor_col() + num - 1;
|
||||
if ( limit >= fb->ds.get_width() ) {
|
||||
limit = fb->ds.get_width() - 1;
|
||||
}
|
||||
|
||||
clearline( fb, -1, fb->ds.get_cursor_col(), limit );
|
||||
}
|
||||
|
||||
static Function func_CSI_ECH( CSI, "X", CSI_ECH );
|
||||
|
||||
/* reset to initial state */
|
||||
void Esc_RIS( Framebuffer *fb, Dispatcher *dispatch __attribute((unused)) )
|
||||
{
|
||||
fb->reset();
|
||||
}
|
||||
|
||||
static Function func_Esc_RIS( ESCAPE, "c", Esc_RIS );
|
||||
|
||||
/* soft reset */
|
||||
void CSI_DECSTR( Framebuffer *fb, Dispatcher *dispatch __attribute((unused)) )
|
||||
{
|
||||
fb->soft_reset();
|
||||
}
|
||||
|
||||
static Function func_CSI_DECSTR( CSI, "!p", CSI_DECSTR );
|
||||
|
||||
/* xterm uses an Operating System Command to set the window title */
|
||||
void Dispatcher::OSC_dispatch( const Parser::OSC_End *act, Framebuffer *fb )
|
||||
{
|
||||
if ( OSC_string.size() >= 2 ) {
|
||||
if ( (OSC_string[ 0 ] == L'0')
|
||||
&& (OSC_string[ 1 ] == L';') ) {
|
||||
std::deque<wchar_t> newtitle( OSC_string.begin(), OSC_string.end() );
|
||||
newtitle.erase( newtitle.begin() );
|
||||
newtitle.erase( newtitle.begin() );
|
||||
fb->set_window_title( newtitle );
|
||||
act->handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* scroll down or terminfo indn */
|
||||
void CSI_SD( Framebuffer *fb, Dispatcher *dispatch )
|
||||
{
|
||||
fb->scroll( dispatch->getparam( 0, 1 ) );
|
||||
}
|
||||
|
||||
static Function func_CSI_SD( CSI, "S", CSI_SD );
|
||||
|
||||
/* scroll up or terminfo rin */
|
||||
void CSI_SU( Framebuffer *fb, Dispatcher *dispatch )
|
||||
{
|
||||
fb->scroll( -dispatch->getparam( 0, 1 ) );
|
||||
}
|
||||
|
||||
static Function func_CSI_SU( CSI, "T", CSI_SU );
|
||||
@@ -0,0 +1,720 @@
|
||||
#include <algorithm>
|
||||
#include <wchar.h>
|
||||
#include <list>
|
||||
#include <typeinfo>
|
||||
#include <limits.h>
|
||||
|
||||
#include "terminaloverlay.hpp"
|
||||
|
||||
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.hpp"
|
||||
#include "network.hpp"
|
||||
#include "parser.hpp"
|
||||
|
||||
#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
|
||||
@@ -0,0 +1,30 @@
|
||||
#include "terminaluserinput.hpp"
|
||||
|
||||
using namespace Terminal;
|
||||
|
||||
std::string UserInput::input( const Parser::UserByte *act,
|
||||
bool application_mode_cursor_keys )
|
||||
{
|
||||
char translated_str[ 2 ] = { act->c, 0 };
|
||||
|
||||
/* The user will always be in application mode. If stm is not in
|
||||
application mode, convert user's cursor control function to an
|
||||
ANSI cursor control sequence */
|
||||
|
||||
/* We don't need lookahead to do this for 7-bit. */
|
||||
|
||||
if ( (!application_mode_cursor_keys)
|
||||
&& (last_byte == 0x1b) /* ESC */
|
||||
&& (act->c == 'O') ) { /* ESC O = 7-bit SS3 = application mode */
|
||||
translated_str[ 0 ] = '[';
|
||||
}
|
||||
|
||||
/* This doesn't handle the 8-bit SS3 C1 control, which would be
|
||||
two octets in UTF-8. Fortunately nobody seems to send this. */
|
||||
|
||||
last_byte = act->c;
|
||||
|
||||
act->handled = true;
|
||||
|
||||
return std::string( translated_str, 1 );
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#ifndef TERMINALUSERINPUT_HPP
|
||||
#define TERMINALUSERINPUT_HPP
|
||||
|
||||
#include <string>
|
||||
#include "parseraction.hpp"
|
||||
|
||||
namespace Terminal {
|
||||
class UserInput {
|
||||
private:
|
||||
wchar_t last_byte;
|
||||
|
||||
public:
|
||||
UserInput()
|
||||
: last_byte( -1 )
|
||||
{}
|
||||
|
||||
std::string input( const Parser::UserByte *act,
|
||||
bool application_mode_cursor_keys );
|
||||
|
||||
bool operator==( const UserInput &x ) const { return last_byte == x.last_byte; }
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,162 @@
|
||||
#include <endian.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include "transportfragment.hpp"
|
||||
#include "transportinstruction.pb.h"
|
||||
|
||||
using namespace Network;
|
||||
using namespace TransportBuffers;
|
||||
|
||||
static string network_order_string( uint16_t host_order )
|
||||
{
|
||||
uint16_t net_int = htobe16( host_order );
|
||||
return string( (char *)&net_int, sizeof( net_int ) );
|
||||
}
|
||||
|
||||
static string network_order_string( uint64_t host_order )
|
||||
{
|
||||
uint64_t net_int = htobe64( host_order );
|
||||
return string( (char *)&net_int, sizeof( net_int ) );
|
||||
}
|
||||
|
||||
string Fragment::tostring( void )
|
||||
{
|
||||
assert( initialized );
|
||||
|
||||
string ret;
|
||||
|
||||
ret += network_order_string( id );
|
||||
|
||||
assert( !( fragment_num & 0x8000 ) ); /* effective limit on size of a terminal screen change or buffered user input */
|
||||
uint16_t combined_fragment_num = ( final << 15 ) | fragment_num;
|
||||
ret += network_order_string( combined_fragment_num );
|
||||
|
||||
assert( ret.size() == frag_header_len );
|
||||
|
||||
ret += contents;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
Fragment::Fragment( string &x )
|
||||
: id( -1 ), fragment_num( -1 ), final( false ), initialized( true ),
|
||||
contents( x.begin() + frag_header_len, x.end() )
|
||||
{
|
||||
assert( x.size() >= frag_header_len );
|
||||
|
||||
uint64_t *data64 = (uint64_t *)x.data();
|
||||
uint16_t *data16 = (uint16_t *)x.data();
|
||||
id = be64toh( data64[ 0 ] );
|
||||
fragment_num = be16toh( data16[ 4 ] );
|
||||
final = ( fragment_num & 0x8000 ) >> 15;
|
||||
fragment_num &= 0x7FFF;
|
||||
}
|
||||
|
||||
bool FragmentAssembly::add_fragment( Fragment &frag )
|
||||
{
|
||||
/* see if this is a totally new packet */
|
||||
if ( current_id != frag.id ) {
|
||||
fragments.clear();
|
||||
fragments.resize( frag.fragment_num + 1 );
|
||||
fragments.at( frag.fragment_num ) = frag;
|
||||
fragments_arrived = 1;
|
||||
fragments_total = -1; /* unknown */
|
||||
current_id = frag.id;
|
||||
} else { /* not a new packet */
|
||||
/* see if we already have this fragment */
|
||||
if ( (fragments.size() > frag.fragment_num)
|
||||
&& (fragments.at( frag.fragment_num ).initialized) ) {
|
||||
/* make sure new version is same as what we already have */
|
||||
assert( fragments.at( frag.fragment_num ) == frag );
|
||||
} else {
|
||||
if ( (int)fragments.size() < frag.fragment_num + 1 ) {
|
||||
fragments.resize( frag.fragment_num + 1 );
|
||||
}
|
||||
fragments.at( frag.fragment_num ) = frag;
|
||||
fragments_arrived++;
|
||||
}
|
||||
}
|
||||
|
||||
if ( frag.final ) {
|
||||
fragments_total = frag.fragment_num + 1;
|
||||
assert( (int)fragments.size() <= fragments_total );
|
||||
fragments.resize( fragments_total );
|
||||
}
|
||||
|
||||
if ( fragments_total != -1 ) {
|
||||
assert( fragments_arrived <= fragments_total );
|
||||
}
|
||||
|
||||
/* see if we're done */
|
||||
return ( fragments_arrived == fragments_total );
|
||||
}
|
||||
|
||||
Instruction FragmentAssembly::get_assembly( void )
|
||||
{
|
||||
assert( fragments_arrived == fragments_total );
|
||||
|
||||
string encoded;
|
||||
|
||||
for ( int i = 0; i < fragments_total; i++ ) {
|
||||
assert( fragments.at( i ).initialized );
|
||||
encoded += fragments.at( i ).contents;
|
||||
}
|
||||
|
||||
Instruction ret;
|
||||
assert( ret.ParseFromString( encoded ) );
|
||||
|
||||
fragments.clear();
|
||||
fragments_arrived = 0;
|
||||
fragments_total = -1;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool Fragment::operator==( const Fragment &x )
|
||||
{
|
||||
return ( id == x.id ) && ( fragment_num == x.fragment_num ) && ( final == x.final )
|
||||
&& ( initialized == x.initialized ) && ( contents == x.contents );
|
||||
}
|
||||
|
||||
vector<Fragment> Fragmenter::make_fragments( Instruction &inst, int MTU )
|
||||
{
|
||||
if ( (inst.old_num() != last_instruction.old_num())
|
||||
|| (inst.new_num() != last_instruction.new_num())
|
||||
|| (inst.ack_num() != last_instruction.ack_num())
|
||||
|| (inst.throwaway_num() != last_instruction.throwaway_num())
|
||||
|| (inst.late_ack_num() != last_instruction.late_ack_num())
|
||||
|| (inst.protocol_version() != last_instruction.protocol_version())
|
||||
|| (last_MTU != MTU) ) {
|
||||
next_instruction_id++;
|
||||
}
|
||||
|
||||
if ( (inst.old_num() == last_instruction.old_num())
|
||||
&& (inst.new_num() == last_instruction.new_num()) ) {
|
||||
assert( inst.diff() == last_instruction.diff() );
|
||||
}
|
||||
|
||||
last_instruction = inst;
|
||||
last_MTU = MTU;
|
||||
|
||||
string payload = inst.SerializeAsString();
|
||||
uint16_t fragment_num = 0;
|
||||
vector<Fragment> ret;
|
||||
|
||||
while ( !payload.empty() ) {
|
||||
string this_fragment;
|
||||
bool final = false;
|
||||
|
||||
if ( int( payload.size() + HEADER_LEN ) > MTU ) {
|
||||
this_fragment = string( payload.begin(), payload.begin() + MTU - HEADER_LEN );
|
||||
payload = string( payload.begin() + MTU - HEADER_LEN, payload.end() );
|
||||
} else {
|
||||
this_fragment = payload;
|
||||
payload.clear();
|
||||
final = true;
|
||||
}
|
||||
|
||||
ret.push_back( Fragment( next_instruction_id, fragment_num++, final, this_fragment ) );
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
#ifndef TRANSPORT_FRAGMENT_HPP
|
||||
#define TRANSPORT_FRAGMENT_HPP
|
||||
|
||||
#include <stdint.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
#include "transportinstruction.pb.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace TransportBuffers;
|
||||
|
||||
namespace Network {
|
||||
static const int HEADER_LEN = 66;
|
||||
|
||||
class Fragment
|
||||
{
|
||||
private:
|
||||
static const size_t frag_header_len = sizeof( uint64_t ) + sizeof( uint16_t );
|
||||
|
||||
public:
|
||||
uint64_t id;
|
||||
uint16_t fragment_num;
|
||||
bool final;
|
||||
|
||||
bool initialized;
|
||||
|
||||
string contents;
|
||||
|
||||
Fragment()
|
||||
: id( -1 ), fragment_num( -1 ), final( false ), initialized( false ), contents()
|
||||
{}
|
||||
|
||||
Fragment( uint64_t s_id, uint16_t s_fragment_num, bool s_final, string s_contents )
|
||||
: id( s_id ), fragment_num( s_fragment_num ), final( s_final ), initialized( true ),
|
||||
contents( s_contents )
|
||||
{}
|
||||
|
||||
Fragment( string &x );
|
||||
|
||||
string tostring( void );
|
||||
|
||||
bool operator==( const Fragment &x );
|
||||
};
|
||||
|
||||
class FragmentAssembly
|
||||
{
|
||||
private:
|
||||
vector<Fragment> fragments;
|
||||
uint64_t current_id;
|
||||
int fragments_arrived, fragments_total;
|
||||
|
||||
public:
|
||||
FragmentAssembly() : fragments(), current_id( -1 ), fragments_arrived( 0 ), fragments_total( -1 ) {}
|
||||
bool add_fragment( Fragment &inst );
|
||||
Instruction get_assembly( void );
|
||||
};
|
||||
|
||||
class Fragmenter
|
||||
{
|
||||
private:
|
||||
uint64_t next_instruction_id;
|
||||
Instruction last_instruction;
|
||||
int last_MTU;
|
||||
|
||||
public:
|
||||
Fragmenter() : next_instruction_id( 0 ), last_instruction(), last_MTU( -1 )
|
||||
{
|
||||
last_instruction.set_old_num( -1 );
|
||||
last_instruction.set_new_num( -1 );
|
||||
}
|
||||
vector<Fragment> make_fragments( Instruction &inst, int MTU );
|
||||
uint64_t last_ack_sent( void ) const { return last_instruction.ack_num(); }
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,301 @@
|
||||
#include <algorithm>
|
||||
#include <list>
|
||||
|
||||
#include "transportsender.hpp"
|
||||
#include "transportfragment.hpp"
|
||||
|
||||
using namespace Network;
|
||||
|
||||
template <class MyState>
|
||||
TransportSender<MyState>::TransportSender( Connection *s_connection, MyState &initial_state )
|
||||
: connection( s_connection ),
|
||||
current_state( initial_state ),
|
||||
sent_states( 1, TimestampedState<MyState>( timestamp(), 0, initial_state ) ),
|
||||
assumed_receiver_state( sent_states.begin() ),
|
||||
fragmenter(),
|
||||
next_ack_time( timestamp() ),
|
||||
next_send_time( timestamp() ),
|
||||
verbose( false ),
|
||||
shutdown_in_progress( false ),
|
||||
shutdown_tries( 0 ),
|
||||
ack_num( 0 ),
|
||||
pending_data_ack( false ),
|
||||
ack_timestamp( 0 ),
|
||||
ack_history(),
|
||||
SEND_MINDELAY( 15 )
|
||||
{
|
||||
}
|
||||
|
||||
/* Try to send roughly two frames per RTT, bounded by limits on frame rate */
|
||||
template <class MyState>
|
||||
unsigned int TransportSender<MyState>::send_interval( void ) const
|
||||
{
|
||||
int SEND_INTERVAL = lrint( ceil( connection->get_SRTT() / 2.0 ) );
|
||||
if ( SEND_INTERVAL < SEND_INTERVAL_MIN ) {
|
||||
SEND_INTERVAL = SEND_INTERVAL_MIN;
|
||||
} else if ( SEND_INTERVAL > SEND_INTERVAL_MAX ) {
|
||||
SEND_INTERVAL = SEND_INTERVAL_MAX;
|
||||
}
|
||||
|
||||
return SEND_INTERVAL;
|
||||
}
|
||||
|
||||
/* How many ms can the caller wait before we will have an event (empty ack or next frame)? */
|
||||
template <class MyState>
|
||||
int TransportSender<MyState>::wait_time( void )
|
||||
{
|
||||
if ( pending_data_ack && (next_ack_time > timestamp() + ACK_DELAY) ) {
|
||||
next_ack_time = timestamp() + ACK_DELAY;
|
||||
}
|
||||
|
||||
if ( !(current_state == sent_states.back().state) ) { /* pending data to send */
|
||||
if ( next_send_time > timestamp() + SEND_MINDELAY ) {
|
||||
next_send_time = timestamp() + SEND_MINDELAY;
|
||||
}
|
||||
|
||||
if ( next_send_time < sent_states.back().timestamp + send_interval() ) {
|
||||
next_send_time = sent_states.back().timestamp + send_interval();
|
||||
}
|
||||
}
|
||||
|
||||
/* speed up shutdown sequence */
|
||||
if ( shutdown_in_progress || (ack_num == uint64_t(-1)) ) {
|
||||
next_ack_time = sent_states.back().timestamp + send_interval();
|
||||
}
|
||||
|
||||
uint64_t next_wakeup = next_ack_time;
|
||||
|
||||
if ( next_send_time < next_wakeup ) {
|
||||
next_wakeup = next_send_time;
|
||||
}
|
||||
|
||||
if ( !connection->get_attached() ) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if ( next_wakeup > timestamp() ) {
|
||||
return next_wakeup - timestamp();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Send data or an empty ack if necessary */
|
||||
template <class MyState>
|
||||
void TransportSender<MyState>::tick( void )
|
||||
{
|
||||
wait_time();
|
||||
|
||||
if ( !connection->get_attached() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( (timestamp() < next_ack_time)
|
||||
&& (timestamp() < next_send_time) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Determine if a new diff or empty ack needs to be sent */
|
||||
/* Update assumed receiver state */
|
||||
update_assumed_receiver_state();
|
||||
|
||||
/* Cut out common prefix of all states */
|
||||
rationalize_states();
|
||||
|
||||
string diff = current_state.diff_from( assumed_receiver_state->state );
|
||||
|
||||
if ( diff.empty() && (timestamp() >= next_ack_time) ) {
|
||||
send_empty_ack();
|
||||
return;
|
||||
}
|
||||
|
||||
if ( !diff.empty() && ( (timestamp() >= next_send_time)
|
||||
|| (timestamp() >= next_ack_time) ) ) {
|
||||
/* Send diffs or ack */
|
||||
send_to_receiver( diff );
|
||||
}
|
||||
}
|
||||
|
||||
template <class MyState>
|
||||
void TransportSender<MyState>::send_empty_ack( void )
|
||||
{
|
||||
assert ( timestamp() >= next_ack_time );
|
||||
|
||||
uint64_t new_num = sent_states.back().num + 1;
|
||||
|
||||
/* special case for shutdown sequence */
|
||||
if ( shutdown_in_progress ) {
|
||||
new_num = uint64_t( -1 );
|
||||
}
|
||||
|
||||
// sent_states.push_back( TimestampedState<MyState>( sent_states.back().timestamp, new_num, current_state ) );
|
||||
add_sent_state( sent_states.back().timestamp, new_num, current_state );
|
||||
send_in_fragments( "", new_num );
|
||||
|
||||
next_ack_time = timestamp() + ACK_INTERVAL;
|
||||
}
|
||||
|
||||
template <class MyState>
|
||||
void TransportSender<MyState>::add_sent_state( uint64_t the_timestamp, uint64_t num, MyState &state )
|
||||
{
|
||||
sent_states.push_back( TimestampedState<MyState>( the_timestamp, num, state ) );
|
||||
if ( sent_states.size() > 32 ) { /* limit on state queue */
|
||||
auto last = sent_states.end();
|
||||
for ( int i = 0; i < 16; i++ ) { last--; }
|
||||
sent_states.erase( last ); /* erase state from middle of queue */
|
||||
}
|
||||
}
|
||||
|
||||
template <class MyState>
|
||||
void TransportSender<MyState>::send_to_receiver( string diff )
|
||||
{
|
||||
uint64_t new_num;
|
||||
if ( current_state == sent_states.back().state ) { /* previously sent */
|
||||
new_num = sent_states.back().num;
|
||||
} else { /* new state */
|
||||
new_num = sent_states.back().num + 1;
|
||||
}
|
||||
|
||||
/* special case for shutdown sequence */
|
||||
if ( shutdown_in_progress ) {
|
||||
new_num = uint64_t( -1 );
|
||||
}
|
||||
|
||||
if ( new_num == sent_states.back().num ) {
|
||||
sent_states.back().timestamp = timestamp();
|
||||
} else {
|
||||
add_sent_state( timestamp(), new_num, current_state );
|
||||
}
|
||||
|
||||
send_in_fragments( diff, new_num ); // Can throw NetworkException
|
||||
|
||||
/* successfully sent, probably */
|
||||
/* ("probably" because the FIRST size-exceeded datagram doesn't get an error) */
|
||||
assumed_receiver_state = sent_states.end();
|
||||
assumed_receiver_state--;
|
||||
next_ack_time = timestamp() + ACK_INTERVAL;
|
||||
next_send_time = uint64_t(-1);
|
||||
}
|
||||
|
||||
template <class MyState>
|
||||
void TransportSender<MyState>::update_assumed_receiver_state( void )
|
||||
{
|
||||
uint64_t now = timestamp();
|
||||
|
||||
/* start from what is known and give benefit of the doubt to unacknowledged states
|
||||
transmitted recently enough ago */
|
||||
assumed_receiver_state = sent_states.begin();
|
||||
|
||||
typename list< TimestampedState<MyState> >::iterator i = sent_states.begin();
|
||||
i++;
|
||||
|
||||
while ( i != sent_states.end() ) {
|
||||
assert( now >= i->timestamp );
|
||||
|
||||
if ( uint64_t(now - i->timestamp) < connection->timeout() + ACK_DELAY ) {
|
||||
assumed_receiver_state = i;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
template <class MyState>
|
||||
void TransportSender<MyState>::rationalize_states( void )
|
||||
{
|
||||
const MyState * known_receiver_state = &sent_states.front().state;
|
||||
|
||||
current_state.subtract( known_receiver_state );
|
||||
|
||||
for ( typename list< TimestampedState<MyState> >::reverse_iterator i = sent_states.rbegin();
|
||||
i != sent_states.rend();
|
||||
i++ ) {
|
||||
i->state.subtract( known_receiver_state );
|
||||
}
|
||||
}
|
||||
|
||||
template <class MyState>
|
||||
void TransportSender<MyState>::send_in_fragments( string diff, uint64_t new_num )
|
||||
{
|
||||
Instruction inst;
|
||||
|
||||
uint64_t now = timestamp();
|
||||
|
||||
inst.set_protocol_version( MOSH_PROTOCOL_VERSION );
|
||||
inst.set_old_num( assumed_receiver_state->num );
|
||||
inst.set_new_num( new_num );
|
||||
inst.set_ack_num( ack_num );
|
||||
inst.set_throwaway_num( sent_states.front().num );
|
||||
inst.set_late_ack_num( get_late_ack( now ) );
|
||||
inst.set_diff( diff );
|
||||
|
||||
if ( new_num == uint64_t(-1) ) {
|
||||
shutdown_tries++;
|
||||
}
|
||||
|
||||
vector<Fragment> fragments = fragmenter.make_fragments( inst, connection->get_MTU() );
|
||||
|
||||
for ( auto i = fragments.begin(); i != fragments.end(); i++ ) {
|
||||
connection->send( i->tostring() );
|
||||
|
||||
if ( verbose ) {
|
||||
fprintf( stderr, "[%u] Sent [%d=>%d] id %d, frag %d ack=%d, late_ack=%d, throwaway=%d, len=%d, frame rate=%.2f, timeout=%d, srtt=%.1f age=%llu\n",
|
||||
(unsigned int)(timestamp() % 100000), (int)inst.old_num(), (int)inst.new_num(), (int)i->id, (int)i->fragment_num,
|
||||
(int)inst.ack_num(), (int)inst.late_ack_num(), (int)inst.throwaway_num(), (int)i->contents.size(),
|
||||
1000.0 / (double)send_interval(),
|
||||
(int)connection->timeout(), connection->get_SRTT(),
|
||||
(long long)(now - ack_timestamp) );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pending_data_ack = false;
|
||||
}
|
||||
|
||||
template <class MyState>
|
||||
void TransportSender<MyState>::process_acknowledgment_through( uint64_t ack_num )
|
||||
{
|
||||
/* Ignore ack if we have culled the state it's acknowledging */
|
||||
|
||||
if ( sent_states.end() != find_if( sent_states.begin(), sent_states.end(),
|
||||
[&]( const TimestampedState<MyState> &x ) { return x.num == ack_num; } ) ) {
|
||||
sent_states.remove_if( [&]( const TimestampedState<MyState> &x ) { return x.num < ack_num; } );
|
||||
}
|
||||
|
||||
assert( !sent_states.empty() );
|
||||
}
|
||||
|
||||
/* give up on getting acknowledgement for shutdown */
|
||||
template <class MyState>
|
||||
bool TransportSender<MyState>::shutdown_ack_timed_out( void ) const
|
||||
{
|
||||
return shutdown_tries >= SHUTDOWN_RETRIES;
|
||||
}
|
||||
|
||||
/* Executed upon entry to new receiver state */
|
||||
template <class MyState>
|
||||
void TransportSender<MyState>::set_ack_num( uint64_t s_ack_num )
|
||||
{
|
||||
ack_num = s_ack_num;
|
||||
ack_timestamp = timestamp();
|
||||
ack_history.push_back( make_pair( ack_num, ack_timestamp ) );
|
||||
}
|
||||
|
||||
/* The "late" ack is for the input state that has had enough time on the host to have been echoed */
|
||||
template <class MyState>
|
||||
uint64_t TransportSender<MyState>::get_late_ack( uint64_t now )
|
||||
{
|
||||
uint64_t newest_echo_ack = 0;
|
||||
|
||||
for ( auto i = ack_history.begin(); i != ack_history.end(); i++ ) {
|
||||
if ( i->second < now - ECHO_TIMEOUT ) {
|
||||
newest_echo_ack = i->first;
|
||||
}
|
||||
}
|
||||
|
||||
ack_history.remove_if( [&]( const pair<uint64_t, uint64_t> &x ) { return x.first < newest_echo_ack; } );
|
||||
|
||||
return newest_echo_ack;
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
|
||||
#ifndef TRANSPORT_SENDER_HPP
|
||||
#define TRANSPORT_SENDER_HPP
|
||||
|
||||
#include <string>
|
||||
#include <list>
|
||||
|
||||
#include "network.hpp"
|
||||
#include "transportinstruction.pb.h"
|
||||
#include "transportstate.hpp"
|
||||
#include "transportfragment.hpp"
|
||||
|
||||
using namespace std;
|
||||
using namespace TransportBuffers;
|
||||
|
||||
namespace Network {
|
||||
template <class MyState>
|
||||
class TransportSender
|
||||
{
|
||||
private:
|
||||
/* timing parameters */
|
||||
static const int SEND_INTERVAL_MIN = 20; /* ms between frames */
|
||||
static const int SEND_INTERVAL_MAX = 250; /* ms between frames */
|
||||
static const int ACK_INTERVAL = 3000; /* ms between empty acks */
|
||||
static const int ACK_DELAY = 100; /* ms before delayed ack */
|
||||
static const int SHUTDOWN_RETRIES = 3; /* number of shutdown packets to send before giving up */
|
||||
static const int ECHO_TIMEOUT = 50; /* for late ack */
|
||||
|
||||
/* helper methods for tick() */
|
||||
void update_assumed_receiver_state( void );
|
||||
void rationalize_states( void );
|
||||
void send_to_receiver( string diff );
|
||||
void send_empty_ack( void );
|
||||
void send_in_fragments( string diff, uint64_t new_num );
|
||||
void add_sent_state( uint64_t the_timestamp, uint64_t num, MyState &state );
|
||||
|
||||
/* state of sender */
|
||||
Connection *connection;
|
||||
|
||||
MyState current_state;
|
||||
|
||||
list< TimestampedState<MyState> > sent_states;
|
||||
/* first element: known, acknowledged receiver state */
|
||||
/* last element: last sent state */
|
||||
|
||||
/* somewhere in the middle: the assumed state of the receiver */
|
||||
typename list< TimestampedState<MyState> >::iterator assumed_receiver_state;
|
||||
|
||||
/* for fragment creation */
|
||||
Fragmenter fragmenter;
|
||||
|
||||
/* timing state */
|
||||
uint64_t next_ack_time;
|
||||
uint64_t next_send_time;
|
||||
|
||||
bool verbose;
|
||||
bool shutdown_in_progress;
|
||||
int shutdown_tries;
|
||||
|
||||
/* information about receiver state */
|
||||
uint64_t ack_num;
|
||||
bool pending_data_ack;
|
||||
uint64_t ack_timestamp;
|
||||
|
||||
list< pair<uint64_t, uint64_t> > ack_history;
|
||||
uint64_t get_late_ack( uint64_t now ); /* calculate delayed "echo" acknowledgment */
|
||||
|
||||
unsigned int SEND_MINDELAY; /* ms to collect all input */
|
||||
|
||||
public:
|
||||
/* constructor */
|
||||
TransportSender( Connection *s_connection, MyState &initial_state );
|
||||
|
||||
/* Send data or an ack if necessary */
|
||||
void tick( void );
|
||||
|
||||
/* Returns the number of ms to wait until next possible event. */
|
||||
int wait_time( void );
|
||||
|
||||
/* Executed upon receipt of ack */
|
||||
void process_acknowledgment_through( uint64_t ack_num );
|
||||
|
||||
/* Executed upon entry to new receiver state */
|
||||
void set_ack_num( uint64_t s_ack_num );
|
||||
|
||||
/* Accelerate reply ack */
|
||||
void set_data_ack( void ) { pending_data_ack = true; }
|
||||
|
||||
/* Starts shutdown sequence */
|
||||
void start_shutdown( void ) { shutdown_in_progress = true; }
|
||||
|
||||
/* Misc. getters and setters */
|
||||
/* Cannot modify current_state while shutdown in progress */
|
||||
MyState &get_current_state( void ) { assert( !shutdown_in_progress ); return current_state; }
|
||||
void set_current_state( const MyState &x ) { assert( !shutdown_in_progress ); current_state = x; }
|
||||
void set_verbose( void ) { verbose = true; }
|
||||
|
||||
bool get_shutdown_in_progress( void ) const { return shutdown_in_progress; }
|
||||
bool get_shutdown_acknowledged( void ) const { return sent_states.front().num == uint64_t(-1); }
|
||||
bool get_counterparty_shutdown_acknowledged( void ) const { return fragmenter.last_ack_sent() == uint64_t(-1); }
|
||||
uint64_t get_sent_state_acked( void ) const { return sent_states.front().num; }
|
||||
uint64_t get_sent_state_last( void ) const { return sent_states.back().num; }
|
||||
|
||||
bool shutdown_ack_timed_out( void ) const;
|
||||
|
||||
void set_send_delay( int new_delay ) { SEND_MINDELAY = new_delay; }
|
||||
|
||||
unsigned int send_interval( void ) const;
|
||||
|
||||
/* nonexistent methods to satisfy -Weffc++ */
|
||||
TransportSender( const TransportSender &x );
|
||||
TransportSender & operator=( const TransportSender &x );
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,19 @@
|
||||
#ifndef TRANSPORT_STATE_HPP
|
||||
#define TRANSPORT_STATE_HPP
|
||||
|
||||
namespace Network {
|
||||
template <class State>
|
||||
class TimestampedState
|
||||
{
|
||||
public:
|
||||
uint64_t timestamp;
|
||||
uint64_t num;
|
||||
State state;
|
||||
|
||||
TimestampedState( uint64_t s_timestamp, uint64_t s_num, State &s_state )
|
||||
: timestamp( s_timestamp ), num( s_num ), state( s_state )
|
||||
{}
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,97 @@
|
||||
#include <assert.h>
|
||||
#include <typeinfo>
|
||||
|
||||
#include "user.hpp"
|
||||
#include "userinput.pb.h"
|
||||
|
||||
using namespace Parser;
|
||||
using namespace Network;
|
||||
using namespace ClientBuffers;
|
||||
|
||||
void UserStream::subtract( const UserStream *prefix )
|
||||
{
|
||||
for ( deque<UserEvent>::const_iterator i = prefix->actions.begin();
|
||||
i != prefix->actions.end();
|
||||
i++ ) {
|
||||
assert( !actions.empty() );
|
||||
assert( *i == actions.front() );
|
||||
actions.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
string UserStream::diff_from( const UserStream &existing )
|
||||
{
|
||||
deque<UserEvent>::iterator my_it = actions.begin();
|
||||
|
||||
for ( deque<UserEvent>::const_iterator i = existing.actions.begin();
|
||||
i != existing.actions.end();
|
||||
i++ ) {
|
||||
assert( my_it != actions.end() );
|
||||
assert( *i == *my_it );
|
||||
my_it++;
|
||||
}
|
||||
|
||||
ClientBuffers::UserMessage output;
|
||||
|
||||
while ( my_it != actions.end() ) {
|
||||
switch ( my_it->type ) {
|
||||
case UserByteType:
|
||||
{
|
||||
char the_byte = my_it->userbyte.c;
|
||||
/* can we combine this with a previous Keystroke? */
|
||||
if ( (output.instruction_size() > 0)
|
||||
&& (output.instruction( output.instruction_size() - 1 ).HasExtension( keystroke )) ) {
|
||||
output.mutable_instruction( output.instruction_size() - 1 )->MutableExtension( keystroke )->mutable_keys()->append( string( &the_byte, 1 ) );
|
||||
} else {
|
||||
Instruction *new_inst = output.add_instruction();
|
||||
new_inst->MutableExtension( keystroke )->set_keys( &the_byte, 1 );
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ResizeType:
|
||||
{
|
||||
Instruction *new_inst = output.add_instruction();
|
||||
new_inst->MutableExtension( resize )->set_width( my_it->resize.width );
|
||||
new_inst->MutableExtension( resize )->set_height( my_it->resize.height );
|
||||
}
|
||||
break;
|
||||
default:
|
||||
assert( false );
|
||||
}
|
||||
|
||||
my_it++;
|
||||
}
|
||||
|
||||
return output.SerializeAsString();
|
||||
}
|
||||
|
||||
void UserStream::apply_string( string diff )
|
||||
{
|
||||
ClientBuffers::UserMessage input;
|
||||
assert( input.ParseFromString( diff ) );
|
||||
|
||||
for ( int i = 0; i < input.instruction_size(); i++ ) {
|
||||
if ( input.instruction( i ).HasExtension( keystroke ) ) {
|
||||
string the_bytes = input.instruction( i ).GetExtension( keystroke ).keys();
|
||||
for ( unsigned int loc = 0; loc < the_bytes.size(); loc++ ) {
|
||||
actions.push_back( UserEvent( UserByte( the_bytes.at( loc ) ) ) );
|
||||
}
|
||||
} else if ( input.instruction( i ).HasExtension( resize ) ) {
|
||||
actions.push_back( UserEvent( Resize( input.instruction( i ).GetExtension( resize ).width(),
|
||||
input.instruction( i ).GetExtension( resize ).height() ) ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Parser::Action *UserStream::get_action( unsigned int i )
|
||||
{
|
||||
switch( actions[ i ].type ) {
|
||||
case UserByteType:
|
||||
return &( actions[ i ].userbyte );
|
||||
case ResizeType:
|
||||
return &( actions[ i ].resize );
|
||||
default:
|
||||
assert( false );
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
#ifndef USER_HPP
|
||||
#define USER_HPP
|
||||
|
||||
#include <deque>
|
||||
#include <list>
|
||||
#include <string>
|
||||
#include <assert.h>
|
||||
|
||||
#include "parseraction.hpp"
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace Network {
|
||||
enum UserEventType {
|
||||
UserByteType = 0,
|
||||
ResizeType = 1
|
||||
};
|
||||
|
||||
class UserEvent
|
||||
{
|
||||
public:
|
||||
UserEventType type;
|
||||
Parser::UserByte userbyte;
|
||||
Parser::Resize resize;
|
||||
|
||||
UserEvent( Parser::UserByte s_userbyte ) : type( UserByteType ), userbyte( s_userbyte ), resize( -1, -1 ) {}
|
||||
UserEvent( Parser::Resize s_resize ) : type( ResizeType ), userbyte( 0 ), resize( s_resize ) {}
|
||||
|
||||
UserEvent() /* default constructor required by C++11 STL */
|
||||
: type( UserByteType ),
|
||||
userbyte( 0 ),
|
||||
resize( -1, -1 )
|
||||
{
|
||||
assert( false );
|
||||
}
|
||||
|
||||
bool operator==( const UserEvent &x ) const { return ( type == x.type ) && ( userbyte == x.userbyte ) && ( resize == x.resize ); }
|
||||
};
|
||||
|
||||
class UserStream
|
||||
{
|
||||
private:
|
||||
deque<UserEvent> actions;
|
||||
|
||||
public:
|
||||
UserStream() : actions() {}
|
||||
|
||||
void push_back( Parser::UserByte s_userbyte ) { actions.push_back( UserEvent( s_userbyte ) ); }
|
||||
void push_back( Parser::Resize s_resize ) { actions.push_back( UserEvent( s_resize ) ); }
|
||||
|
||||
size_t size( void ) { return actions.size(); }
|
||||
const Parser::Action *get_action( unsigned int i );
|
||||
|
||||
/* interface for Network::Transport */
|
||||
void subtract( const UserStream *prefix );
|
||||
string diff_from( const UserStream &existing );
|
||||
void apply_string( string diff );
|
||||
bool operator==( const UserStream &x ) const { return actions == x.actions; }
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user