/* * %CopyrightBegin% * * Copyright Ericsson AB 1996-2018. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * %CopyrightEnd% */ /* * Tty driver that reads one character at the time and provides a * smart line for output. */ #ifdef HAVE_CONFIG_H # include "config.h" #endif #include "erl_driver.h" static int ttysl_init(void); static ErlDrvData ttysl_start(ErlDrvPort, char*); #ifdef HAVE_TERMCAP /* else make an empty driver that can not be opened */ #ifndef WANT_NONBLOCKING #define WANT_NONBLOCKING #endif #include "sys.h" #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_WCWIDTH #include #endif #ifdef HAVE_FCNTL_H #include #endif #ifdef HAVE_SYS_IOCTL_H #include #endif #if !defined(HAVE_SETLOCALE) || !defined(HAVE_NL_LANGINFO) || !defined(HAVE_LANGINFO_H) #define PRIMITIVE_UTF8_CHECK 1 #else #include #endif #if defined IOV_MAX #define MAXIOV IOV_MAX #elif defined UIO_MAXIOV #define MAXIOV UIO_MAXIOV #else #define MAXIOV 16 #endif #define TRUE 1 #define FALSE 0 /* Termcap functions. */ int tgetent(char* bp, char *name); int tgetnum(char* cap); int tgetflag(char* cap); char *tgetstr(char* cap, char** buf); char *tgoto(char* cm, int col, int line); int tputs(char* cp, int affcnt, int (*outc)(int c)); /* Terminal capabilites in which we are interested. */ static char *capbuf; static char *up, *down, *left, *right; static int cols, xn; static volatile int cols_needs_update = FALSE; /* The various opcodes. */ #define OP_PUTC 0 #define OP_MOVE 1 #define OP_INSC 2 #define OP_DELC 3 #define OP_BEEP 4 #define OP_PUTC_SYNC 5 /* Control op */ #define CTRL_OP_GET_WINSIZE 100 #define CTRL_OP_GET_UNICODE_STATE 101 #define CTRL_OP_SET_UNICODE_STATE 102 /* We use 1024 as the buf size as that was the default buf size of FILE streams on all platforms that I checked. */ #define TTY_BUFFSIZE 1024 static int lbuf_size = BUFSIZ; static Uint32 *lbuf; /* The current line buffer */ static int llen; /* The current line length */ static int lpos; /* The current "cursor position" in the line buffer */ /* NOTE: not the same as column position a char may not take a" * column to display or it might take many columns */ /* * Tags used in line buffer to show that these bytes represent special characters, * Max unicode is 0x0010ffff, so we have lots of place for meta tags... */ #define CONTROL_TAG 0x10000000U /* Control character, value in first position */ #define ESCAPED_TAG 0x01000000U /* Escaped character, value in first position */ #define TAG_MASK 0xFF000000U #define MAXSIZE (1 << 16) #define COL(_l) ((_l) % cols) #define LINE(_l) ((_l) / cols) #define NL '\n' /* Main interface functions. */ static void ttysl_stop(ErlDrvData); static void ttysl_from_erlang(ErlDrvData, char*, ErlDrvSizeT); static void ttysl_to_tty(ErlDrvData, ErlDrvEvent); static void ttysl_flush_tty(ErlDrvData); static void ttysl_from_tty(ErlDrvData, ErlDrvEvent); static void ttysl_stop_select(ErlDrvEvent, void*); static Sint16 get_sint16(char*); static ErlDrvPort ttysl_port; static int ttysl_fd; static int ttysl_terminate = 0; static int ttysl_send_ok = 0; static ErlDrvBinary *putcbuf; static int putcpos; static int putclen; /* Functions that work on the line buffer. */ static int start_lbuf(void); static int stop_lbuf(void); static int put_chars(byte*,int); static int move_rel(int); static int ins_chars(byte *,int); static int del_chars(int); static int step_over_chars(int); static int insert_buf(byte*,int); static int write_buf(Uint32 *,int); static int outc(int c); static int move_cursor(int,int); static int cp_pos_to_col(int cp_pos); /* Termcap functions. */ static int start_termcap(void); static int stop_termcap(void); static int move_left(int); static int move_right(int); static int move_up(int); static int move_down(int); static void update_cols(void); /* Terminal setting functions. */ static int tty_init(int,int,int,int); static int tty_set(int); static int tty_reset(int); static ErlDrvSSizeT ttysl_control(ErlDrvData, unsigned int, char *, ErlDrvSizeT, char **, ErlDrvSizeT); #ifdef ERTS_NOT_USED static RETSIGTYPE suspend(int); #endif static RETSIGTYPE cont(int); static RETSIGTYPE winch(int); /*#define LOG_DEBUG*/ #ifdef LOG_DEBUG FILE *debuglog = NULL; #define DEBUGLOG(X) \ do { \ if (debuglog != NULL) { \ my_debug_printf X; \ } \ } while (0) static void my_debug_printf(char *fmt, ...) { char buffer[1024]; va_list args; va_start(args, fmt); erts_vsnprintf(buffer,1024,fmt,args); va_end(args); erts_fprintf(debuglog,"%s\n",buffer); /*erts_printf("Debuglog = %s\n",buffer);*/ } #else #define DEBUGLOG(X) #endif static int utf8_mode = 0; static byte utf8buf[4]; /* for incomplete input */ static int utf8buf_size; /* size of incomplete input */ # define IF_IMPL(x) x #else # define IF_IMPL(x) NULL #endif /* HAVE_TERMCAP */ /* Define the driver table entry. */ struct erl_drv_entry ttsl_driver_entry = { ttysl_init, ttysl_start, IF_IMPL(ttysl_stop), IF_IMPL(ttysl_from_erlang), IF_IMPL(ttysl_from_tty), IF_IMPL(ttysl_to_tty), "tty_sl", /* driver_name */ NULL, /* finish */ NULL, /* handle */ IF_IMPL(ttysl_control), NULL, /* timeout */ NULL, /* outputv */ NULL, /* ready_async */ IF_IMPL(ttysl_flush_tty), NULL, /* call */ NULL, /* event */ ERL_DRV_EXTENDED_MARKER, ERL_DRV_EXTENDED_MAJOR_VERSION, ERL_DRV_EXTENDED_MINOR_VERSION, 0, /* ERL_DRV_FLAGs */ NULL, /* handle2 */ NULL, /* process_exit */ IF_IMPL(ttysl_stop_select) }; static int ttysl_init(void) { #ifdef HAVE_TERMCAP ttysl_port = (ErlDrvPort)-1; ttysl_fd = -1; lbuf = NULL; /* For line buffer handling */ capbuf = NULL; /* For termcap handling */ #endif #ifdef LOG_DEBUG { char *dl; if ((dl = getenv("TTYSL_DEBUG_LOG")) != NULL && *dl) { debuglog = fopen(dl,"w+"); if (debuglog != NULL) setbuf(debuglog,NULL); } DEBUGLOG(("ttysl_init: Debuglog = %s(0x%ld)\n",dl,(long) debuglog)); } #endif return 0; } static ErlDrvData ttysl_start(ErlDrvPort port, char* buf) { #ifndef HAVE_TERMCAP return ERL_DRV_ERROR_GENERAL; #else char *s, *t, *l; int canon, echo, sig; /* Terminal characteristics */ int flag; extern int using_oldshell; /* set this to let the rest of erts know */ DEBUGLOG(("ttysl_start: driver input \"%s\", ttysl_port = %d (-1 expected)", buf, ttysl_port)); utf8buf_size = 0; if (ttysl_port != (ErlDrvPort)-1) { DEBUGLOG(("ttysl_start: failure with ttysl_port = %d, not initialized properly?\n", ttysl_port)); return ERL_DRV_ERROR_GENERAL; } DEBUGLOG(("ttysl_start: isatty(0) = %d (1 expected), isatty(1) = %d (1 expected)", isatty(0), isatty(1))); if (!isatty(0) || !isatty(1)) { DEBUGLOG(("ttysl_start: failure in isatty, isatty(0) = %d, isatty(1) = %d", isatty(0), isatty(1))); return ERL_DRV_ERROR_GENERAL; } /* Set the terminal modes to default leave as is. */ canon = echo = sig = 0; /* Parse the input parameters. */ for (s = strchr(buf, ' '); s; s = t) { s++; /* Find end of this argument (start of next) and insert NUL. */ if ((t = strchr(s, ' '))) { *t = '\0'; } if ((flag = ((*s == '+') ? 1 : ((*s == '-') ? -1 : 0)))) { if (s[1] == 'c') canon = flag; if (s[1] == 'e') echo = flag; if (s[1] == 's') sig = flag; } else if ((ttysl_fd = open(s, O_RDWR, 0)) < 0) { DEBUGLOG(("ttysl_start: failed to open ttysl_fd, open(%s, O_RDWR, 0)) = %d\n", s, ttysl_fd)); return ERL_DRV_ERROR_GENERAL; } } if (ttysl_fd < 0) ttysl_fd = 0; if (tty_init(ttysl_fd, canon, echo, sig) < 0 || tty_set(ttysl_fd) < 0) { DEBUGLOG(("ttysl_start: failed init tty or set tty\n")); ttysl_port = (ErlDrvPort)-1; tty_reset(ttysl_fd); return ERL_DRV_ERROR_GENERAL; } /* Set up smart line and termcap stuff. */ if (!start_lbuf() || !start_termcap()) { DEBUGLOG(("ttysl_start: failed to start_lbuf or start_termcap\n")); stop_lbuf(); /* Must free this */ tty_reset(ttysl_fd); return ERL_DRV_ERROR_GENERAL; } SET_NONBLOCKING(ttysl_fd); #ifdef PRIMITIVE_UTF8_CHECK setlocale(LC_CTYPE, ""); /* Set international environment, ignore result */ if (((l = getenv("LC_ALL")) && *l) || ((l = getenv("LC_CTYPE")) && *l) || ((l = getenv("LANG")) && *l)) { if (strstr(l, "UTF-8")) utf8_mode = 1; } #else l = setlocale(LC_CTYPE, ""); /* Set international environment */ if (l != NULL) { utf8_mode = (strcmp(nl_langinfo(CODESET), "UTF-8") == 0); DEBUGLOG(("ttysl_start: setlocale: %s",l)); } #endif DEBUGLOG(("ttysl_start: utf8_mode is %s",(utf8_mode) ? "on" : "off")); sys_signal(SIGCONT, cont); sys_signal(SIGWINCH, winch); driver_select(port, (ErlDrvEvent)(UWord)ttysl_fd, ERL_DRV_READ|ERL_DRV_USE, 1); ttysl_port = port; /* we need to know this when we enter the break handler */ using_oldshell = 0; DEBUGLOG(("ttysl_start: successful start\n")); return (ErlDrvData)ttysl_port; /* Nothing important to return */ #endif /* HAVE_TERMCAP */ } #ifdef HAVE_TERMCAP #define DEF_HEIGHT 24 #define DEF_WIDTH 80 static void ttysl_get_window_size(Uint32 *width, Uint32 *height) { #ifdef TIOCGWINSZ struct winsize ws; if (ioctl(ttysl_fd,TIOCGWINSZ,&ws) == 0) { *width = (Uint32) ws.ws_col; *height = (Uint32) ws.ws_row; if (*width <= 0) *width = DEF_WIDTH; if (*height <= 0) *height = DEF_HEIGHT; return; } #endif *width = DEF_WIDTH; *height = DEF_HEIGHT; } static ErlDrvSSizeT ttysl_control(ErlDrvData drv_data, unsigned int command, char *buf, ErlDrvSizeT len, char **rbuf, ErlDrvSizeT rlen) { char resbuff[2*sizeof(Uint32)]; ErlDrvSizeT res_size; command -= ERTS_TTYSL_DRV_CONTROL_MAGIC_NUMBER; switch (command) { case CTRL_OP_GET_WINSIZE: { Uint32 w,h; ttysl_get_window_size(&w,&h); memcpy(resbuff,&w,sizeof(Uint32)); memcpy(resbuff+sizeof(Uint32),&h,sizeof(Uint32)); res_size = 2*sizeof(Uint32); } break; case CTRL_OP_GET_UNICODE_STATE: *resbuff = (utf8_mode) ? 1 : 0; res_size = 1; break; case CTRL_OP_SET_UNICODE_STATE: if (len > 0) { int m = (int) *buf; *resbuff = (utf8_mode) ? 1 : 0; res_size = 1; utf8_mode = (m) ? 1 : 0; } else { return 0; } break; default: return -1; } if (rlen < res_size) { *rbuf = driver_alloc(res_size); } memcpy(*rbuf,resbuff,res_size); return res_size; } static void ttysl_stop(ErlDrvData ttysl_data) { DEBUGLOG(("ttysl_stop: ttysl_port = %d\n",ttysl_port)); if (ttysl_port != (ErlDrvPort)-1) { stop_lbuf(); stop_termcap(); tty_reset(ttysl_fd); driver_select(ttysl_port, (ErlDrvEvent)(UWord)ttysl_fd, ERL_DRV_WRITE|ERL_DRV_READ|ERL_DRV_USE, 0); sys_signal(SIGCONT, SIG_DFL); sys_signal(SIGWINCH, SIG_DFL); } ttysl_port = (ErlDrvPort)-1; ttysl_fd = -1; ttysl_terminate = 0; /* return TRUE; */ } static int put_utf8(int ch, byte *target, int sz, int *pos) { Uint x = (Uint) ch; if (x < 0x80) { if (*pos >= sz) { return -1; } target[(*pos)++] = (byte) x; } else if (x < 0x800) { if (((*pos) + 1) >= sz) { return -1; } target[(*pos)++] = (((byte) (x >> 6)) | ((byte) 0xC0)); target[(*pos)++] = (((byte) (x & 0x3F)) | ((byte) 0x80)); } else if (x < 0x10000) { if ((x >= 0xD800 && x <= 0xDFFF) || (x == 0xFFFE) || (x == 0xFFFF)) { /* Invalid unicode range */ return -1; } if (((*pos) + 2) >= sz) { return -1; } target[(*pos)++] = (((byte) (x >> 12)) | ((byte) 0xE0)); target[(*pos)++] = ((((byte) (x >> 6)) & 0x3F) | ((byte) 0x80)); target[(*pos)++] = (((byte) (x & 0x3F)) | ((byte) 0x80)); } else if (x < 0x110000) { /* Standard imposed max */ if (((*pos) + 3) >= sz) { return -1; } target[(*pos)++] = (((byte) (x >> 18)) | ((byte) 0xF0)); target[(*pos)++] = ((((byte) (x >> 12)) & 0x3F) | ((byte) 0x80)); target[(*pos)++] = ((((byte) (x >> 6)) & 0x3F) | ((byte) 0x80)); target[(*pos)++] = (((byte) (x & 0x3F)) | ((byte) 0x80)); } else { return -1; } return 0; } static int pick_utf8(byte *s, int sz, int *pos) { int size = sz - (*pos); byte *source; Uint unipoint; if (size > 0) { source = s + (*pos); if (((*source) & ((byte) 0x80)) == 0) { unipoint = (int) *source; ++(*pos); return (int) unipoint; } else if (((*source) & ((byte) 0xE0)) == 0xC0) { if (size < 2) { return -2; } if (((source[1] & ((byte) 0xC0)) != 0x80) || ((*source) < 0xC2) /* overlong */) { return -1; } (*pos) += 2; unipoint = (((Uint) ((*source) & ((byte) 0x1F))) << 6) | ((Uint) (source[1] & ((byte) 0x3F))); return (int) unipoint; } else if (((*source) & ((byte) 0xF0)) == 0xE0) { if (size < 3) { return -2; } if (((source[1] & ((byte) 0xC0)) != 0x80) || ((source[2] & ((byte) 0xC0)) != 0x80) || (((*source) == 0xE0) && (source[1] < 0xA0)) /* overlong */ ) { return -1; } if ((((*source) & ((byte) 0xF)) == 0xD) && ((source[1] & 0x20) != 0)) { return -1; } if (((*source) == 0xEF) && (source[1] == 0xBF) && ((source[2] == 0xBE) || (source[2] == 0xBF))) { return -1; } (*pos) += 3; unipoint = (((Uint) ((*source) & ((byte) 0xF))) << 12) | (((Uint) (source[1] & ((byte) 0x3F))) << 6) | ((Uint) (source[2] & ((byte) 0x3F))); return (int) unipoint; } else if (((*source) & ((byte) 0xF8)) == 0xF0) { if (size < 4) { return -2 ; } if (((source[1] & ((byte) 0xC0)) != 0x80) || ((source[2] & ((byte) 0xC0)) != 0x80) || ((source[3] & ((byte) 0xC0)) != 0x80) || (((*source) == 0xF0) && (source[1] < 0x90)) /* overlong */) { return -1; } if ((((*source) & ((byte)0x7)) > 0x4U) || ((((*source) & ((byte)0x7)) == 0x4U) && ((source[1] & ((byte)0x3F)) > 0xFU))) { return -1; } (*pos) += 4; unipoint = (((Uint) ((*source) & ((byte) 0x7))) << 18) | (((Uint) (source[1] & ((byte) 0x3F))) << 12) | (((Uint) (source[2] & ((byte) 0x3F))) << 6) | ((Uint) (source[3] & ((byte) 0x3F))); return (int) unipoint; } else { return -1; } } else { return -1; } } static int octal_or_hex_positions(Uint c) { int x = 0; Uint ch = c; if (!ch) { return 1; } while(ch) { ++x; ch >>= 3; } if (x <= 3) { return 3; } /* \x{H ...} format when larger than \777 */ x = 0; ch = c; while(ch) { ++x; ch >>= 4; } return x+3; } static void octal_or_hex_format(Uint ch, byte *buf, int *pos) { static byte hex_chars[] = { '0','1','2','3','4','5','6','7','8','9', 'A','B','C','D','E','F'}; int num = octal_or_hex_positions(ch); if (num != 3) { buf[(*pos)++] = 'x'; buf[(*pos)++] = '{'; num -= 3; while(num--) { buf[(*pos)++] = hex_chars[((ch >> (4*num)) & 0xFU)]; } buf[(*pos)++] = '}'; } else { while(num--) { buf[(*pos)++] = ((byte) ((ch >> (3*num)) & 0x7U) + '0'); } } } /* * Check that there is enough room in all buffers to copy all pad chars * and stiff we need If not, realloc lbuf. */ static int check_buf_size(byte *s, int n) { int pos = 0; int ch; int size = 10; DEBUGLOG(("check_buf_size: n = %d",n)); while(pos < n) { /* Indata is always UTF-8 */ if ((ch = pick_utf8(s,n,&pos)) < 0) { /* XXX temporary allow invalid chars */ ch = (int) s[pos]; DEBUGLOG(("check_buf_size: Invalid UTF8:%d",ch)); ++pos; } if (utf8_mode) { /* That is, terminal is UTF8 compliant */ if (ch >= 128 || isprint(ch)) { #ifdef HAVE_WCWIDTH int width; #endif DEBUGLOG(("check_buf_size: Printable(UTF-8:%d):%d",pos,ch)); size++; #ifdef HAVE_WCWIDTH if ((width = wcwidth(ch)) > 1) { size += width - 1; } #endif } else if (ch == '\t') { size += 8; } else { DEBUGLOG(("check_buf_size: Magic(UTF-8:%d):%d",pos,ch)); size += 2; } } else { if (ch <= 255 && isprint(ch)) { DEBUGLOG(("check_buf_size: Printable:%d",ch)); size++; } else if (ch == '\t') size += 8; else if (ch >= 128) { DEBUGLOG(("check_buf_size: Non printable:%d",ch)); size += (octal_or_hex_positions(ch) + 1); } else { DEBUGLOG(("check_buf_size: Magic:%d",ch)); size += 2; } } } if (size + lpos >= lbuf_size) { lbuf_size = size + lpos + BUFSIZ; if ((lbuf = driver_realloc(lbuf, lbuf_size * sizeof(Uint32))) == NULL) { DEBUGLOG(("check_buf_size: alloc failure of %d bytes", lbuf_size * sizeof(Uint32))); driver_failure(ttysl_port, -1); return(0); } } DEBUGLOG(("check_buf_size: success\n")); return(1); } static void ttysl_from_erlang(ErlDrvData ttysl_data, char* buf, ErlDrvSizeT count) { ErlDrvSizeT sz; sz = driver_sizeq(ttysl_port); putclen = count > TTY_BUFFSIZE ? TTY_BUFFSIZE : count; putcbuf = driver_alloc_binary(putclen); putcpos = 0; if (lpos > MAXSIZE) put_chars((byte*)"\n", 1); DEBUGLOG(("ttysl_from_erlang: OP = %d", buf[0])); switch (buf[0]) { case OP_PUTC_SYNC: /* Using sync means that we have to send an ok to the controlling process for each command call. We delay sending ok if the driver queue exceeds a certain size. We do not set ourselves as a busy port, as this could be very bad for user_drv, if it gets blocked on the port_command. */ /* fall through */ case OP_PUTC: DEBUGLOG(("ttysl_from_erlang: OP: Putc(%lu)",(unsigned long) count-1)); if (check_buf_size((byte*)buf+1, count-1) == 0) return; put_chars((byte*)buf+1, count-1); break; case OP_MOVE: move_rel(get_sint16(buf+1)); break; case OP_INSC: if (check_buf_size((byte*)buf+1, count-1) == 0) return; ins_chars((byte*)buf+1, count-1); break; case OP_DELC: del_chars(get_sint16(buf+1)); break; case OP_BEEP: outc('\007'); break; default: /* Unknown op, just ignore. */ break; } driver_enq_bin(ttysl_port,putcbuf,0,putcpos); driver_free_binary(putcbuf); if (sz == 0) { for (;;) { int written, qlen; SysIOVec *iov; iov = driver_peekq(ttysl_port,&qlen); if (iov) written = writev(ttysl_fd, iov, qlen > MAXIOV ? MAXIOV : qlen); else written = 0; if (written < 0) { if (errno == ERRNO_BLOCK || errno == EINTR) { driver_select(ttysl_port,(ErlDrvEvent)(long)ttysl_fd, ERL_DRV_USE|ERL_DRV_WRITE,1); break; } else { DEBUGLOG(("ttysl_from_erlang: driver failure in writev(%d,..) = %d (errno = %d)\n", ttysl_fd, written, errno)); driver_failure_posix(ttysl_port, errno); return; } } else { if (driver_deq(ttysl_port, written) == 0) break; } } } if (buf[0] == OP_PUTC_SYNC) { if (driver_sizeq(ttysl_port) > TTY_BUFFSIZE && !ttysl_terminate) { /* We delay sending the ack until the buffer has been consumed */ ttysl_send_ok = 1; } else { ErlDrvTermData spec[] = { ERL_DRV_PORT, driver_mk_port(ttysl_port), ERL_DRV_ATOM, driver_mk_atom("ok"), ERL_DRV_TUPLE, 2 }; ASSERT(ttysl_send_ok == 0); erl_drv_output_term(driver_mk_port(ttysl_port), spec, sizeof(spec) / sizeof(spec[0])); } } return; /* TRUE; */ } static void ttysl_to_tty(ErlDrvData ttysl_data, ErlDrvEvent fd) { for (;;) { int written, qlen; SysIOVec *iov; ErlDrvSizeT sz; iov = driver_peekq(ttysl_port,&qlen); DEBUGLOG(("ttysl_to_tty: qlen = %d", qlen)); if (iov) written = writev(ttysl_fd, iov, qlen > MAXIOV ? MAXIOV : qlen); else written = 0; if (written < 0) { if (errno == EINTR) { continue; } else if (errno != ERRNO_BLOCK){ DEBUGLOG(("ttysl_to_tty: driver failure in writev(%d,..) = %d (errno = %d)\n", ttysl_fd, written, errno)); driver_failure_posix(ttysl_port, errno); } break; } else { sz = driver_deq(ttysl_port, written); if (sz < TTY_BUFFSIZE && ttysl_send_ok) { ErlDrvTermData spec[] = { ERL_DRV_PORT, driver_mk_port(ttysl_port), ERL_DRV_ATOM, driver_mk_atom("ok"), ERL_DRV_TUPLE, 2 }; ttysl_send_ok = 0; erl_drv_output_term(driver_mk_port(ttysl_port), spec, sizeof(spec) / sizeof(spec[0])); } if (sz == 0) { driver_select(ttysl_port,(ErlDrvEvent)(long)ttysl_fd, ERL_DRV_WRITE,0); if (ttysl_terminate) { /* flush has been called, which means we should terminate when queue is empty. This will not send any exit message */ DEBUGLOG(("ttysl_to_tty: ttysl_terminate normal\n")); driver_failure_atom(ttysl_port, "normal"); } break; } } } return; } static void ttysl_flush_tty(ErlDrvData ttysl_data) { DEBUGLOG(("ttysl_flush_tty: ..")); ttysl_terminate = 1; return; } static void ttysl_from_tty(ErlDrvData ttysl_data, ErlDrvEvent fd) { byte b[1024]; ssize_t i; int ch = 0, pos = 0; int left = 1024; byte *p = b; byte t[1024]; int tpos; if (utf8buf_size > 0) { memcpy(b,utf8buf,utf8buf_size); left -= utf8buf_size; p += utf8buf_size; utf8buf_size = 0; } DEBUGLOG(("ttysl_from_tty: remainder = %d", left)); if ((i = read((int)(SWord)fd, (char *) p, left)) >= 0) { if (p != b) { i += (p - b); } if (utf8_mode) { /* Hopefully an UTF8 terminal */ while(pos < i && (ch = pick_utf8(b,i,&pos)) >= 0) ; if (ch == -2 && i - pos <= 4) { /* bytes left to care for */ utf8buf_size = i -pos; memcpy(utf8buf,b+pos,utf8buf_size); } else if (ch == -1) { DEBUGLOG(("ttysl_from_tty: Giving up on UTF8 mode, invalid character")); utf8_mode = 0; goto latin_terminal; } driver_output(ttysl_port, (char *) b, pos); } else { latin_terminal: tpos = 0; while (pos < i) { while (tpos < 1020 && pos < i) { /* Max 4 bytes for UTF8 */ put_utf8((int) b[pos++], t, 1024, &tpos); } driver_output(ttysl_port, (char *) t, tpos); tpos = 0; } } } else if (errno != EAGAIN && errno != EWOULDBLOCK) { DEBUGLOG(("ttysl_from_tty: driver failure in read(%d,..) = %d (errno = %d)\n", (int)(SWord)fd, i, errno)); driver_failure(ttysl_port, -1); } } static void ttysl_stop_select(ErlDrvEvent e, void* _) { int fd = (int)(long)e; if (fd != 0) { close(fd); } } /* Procedures for putting and getting integers to/from strings. */ static Sint16 get_sint16(char *s) { return ((*s << 8) | ((byte*)s)[1]); } static int start_lbuf(void) { if (!lbuf && !(lbuf = ( Uint32*) driver_alloc(lbuf_size * sizeof(Uint32)))) return FALSE; llen = 0; lpos = 0; return TRUE; } static int stop_lbuf(void) { if (lbuf) { driver_free(lbuf); lbuf = NULL; } return TRUE; } /* Put l bytes (in UTF8) from s into the buffer and output them. */ static int put_chars(byte *s, int l) { int n; n = insert_buf(s, l); if (lpos > llen) llen = lpos; if (n > 0) write_buf(lbuf + lpos - n, n); return TRUE; } /* * Move the current postition forwards or backwards within the current * line. We know about padding. */ static int move_rel(int n) { int npos; /* The new position */ /* Step forwards or backwards over the buffer. */ npos = step_over_chars(n); /* Calculate move, updates pointers and move the cursor. */ move_cursor(lpos, npos); lpos = npos; return TRUE; } /* Insert characters into the buffer at the current position. */ static int ins_chars(byte *s, int l) { int n, tl; Uint32 *tbuf = NULL; /* Suppress warning about use-before-set */ /* Move tail of buffer to make space. */ if ((tl = llen - lpos) > 0) { if ((tbuf = driver_alloc(tl * sizeof(Uint32))) == NULL) return FALSE; memcpy(tbuf, lbuf + lpos, tl * sizeof(Uint32)); } n = insert_buf(s, l); if (tl > 0) { memcpy(lbuf + lpos, tbuf, tl * sizeof(Uint32)); driver_free(tbuf); } llen += n; write_buf(lbuf + (lpos - n), llen - (lpos - n)); move_cursor(llen, lpos); return TRUE; } /* * Delete characters in the buffer. Can delete characters before (n < 0) * and after (n > 0) the current position. Cursor left at beginning of * deleted block. */ static int del_chars(int n) { int i, l, r; int pos; int gcs; /* deleted grapheme characters */ update_cols(); /* Step forward or backwards over n logical characters. */ pos = step_over_chars(n); DEBUGLOG(("del_chars: %d from %d %d %d\n", n, lpos, pos, llen)); if (pos > lpos) { l = pos - lpos; /* Buffer characters to delete */ r = llen - lpos - l; /* Characters after deleted */ gcs = cp_pos_to_col(pos) - cp_pos_to_col(lpos); /* Fix up buffer and buffer pointers. */ if (r > 0) memmove(lbuf + lpos, lbuf + pos, r * sizeof(Uint32)); llen -= l; /* Write out characters after, blank the tail and jump back to lpos. */ write_buf(lbuf + lpos, r); for (i = gcs ; i > 0; --i) outc(' '); if (xn && COL(cp_pos_to_col(llen)+gcs) == 0) { outc(' '); move_left(1); } move_cursor(llen + gcs, lpos); } else if (pos < lpos) { l = lpos - pos; /* Buffer characters */ r = llen - lpos; /* Characters after deleted */ gcs = -move_cursor(lpos, lpos-l); /* Move back */ /* Fix up buffer and buffer pointers. */ if (r > 0) memmove(lbuf + pos, lbuf + lpos, r * sizeof(Uint32)); lpos -= l; llen -= l; /* Write out characters after, blank the tail and jump back to lpos. */ write_buf(lbuf + lpos, r); for (i = gcs ; i > 0; --i) outc(' '); if (xn && COL(cp_pos_to_col(llen)+gcs) == 0) { outc(' '); move_left(1); } move_cursor(llen + gcs, lpos); } return TRUE; } /* Step over n logical characters, check for overflow. */ static int step_over_chars(int n) { Uint32 *c, *beg, *end; beg = lbuf; end = lbuf + llen; c = lbuf + lpos; for ( ; n > 0 && c < end; --n) { c++; while (c < end && (*c & TAG_MASK) && ((*c & ~TAG_MASK) == 0)) c++; } for ( ; n < 0 && c > beg; n++) { --c; while (c > beg && (*c & TAG_MASK) && ((*c & ~TAG_MASK) == 0)) --c; } return c - lbuf; } /* * Insert n characters into the buffer at lpos. * Know about pad characters and treat \n specially. */ static int insert_buf(byte *s, int n) { int pos = 0; int buffpos = lpos; int ch; while (pos < n) { if ((ch = pick_utf8(s,n,&pos)) < 0) { /* XXX temporary allow invalid chars */ ch = (int) s[pos]; DEBUGLOG(("insert_buf: Invalid UTF8:%d",ch)); ++pos; } if ((utf8_mode && (ch >= 128 || isprint(ch))) || (ch <= 255 && isprint(ch))) { DEBUGLOG(("insert_buf: Printable(UTF-8):%d",ch)); lbuf[lpos++] = (Uint32) ch; } else if (ch >= 128) { /* not utf8 mode */ int nc = octal_or_hex_positions(ch); lbuf[lpos++] = ((Uint32) ch) | ESCAPED_TAG; while (nc--) { lbuf[lpos++] = ESCAPED_TAG; } } else if (ch == '\t') { do { lbuf[lpos++] = (CONTROL_TAG | ((Uint32) ch)); ch = 0; } while (lpos % 8); } else if (ch == '\e') { DEBUGLOG(("insert_buf: ANSI Escape: \\e")); lbuf[lpos++] = (CONTROL_TAG | ((Uint32) ch)); } else if (ch == '\n' || ch == '\r') { write_buf(lbuf + buffpos, lpos - buffpos); outc('\r'); if (ch == '\n') outc('\n'); if (llen > lpos) { memcpy(lbuf, lbuf + lpos, llen - lpos); } llen -= lpos; lpos = buffpos = 0; } else { DEBUGLOG(("insert_buf: Magic(UTF-8):%d",ch)); lbuf[lpos++] = ch | CONTROL_TAG; lbuf[lpos++] = CONTROL_TAG; } } return lpos - buffpos; /* characters "written" into current buffer (may be less due to newline) */ } /* * Write n characters in line buffer starting at s. Be smart about * non-printables. Know about pad characters and that \n can never * occur normally. */ static int write_buf(Uint32 *s, int n) { byte ubuf[4]; int ubytes = 0, i; byte lastput = ' '; update_cols(); while (n > 0) { if (!(*s & TAG_MASK) ) { if (utf8_mode) { ubytes = 0; if (put_utf8((int) *s, ubuf, 4, &ubytes) == 0) { for (i = 0; i < ubytes; ++i) { outc(ubuf[i]); } lastput = 0; /* Means the last written character was multibyte UTF8 */ } } else { outc((byte) *s); lastput = (byte) *s; } --n; ++s; } else if (*s == (CONTROL_TAG | ((Uint32) '\t'))) { outc(lastput = ' '); --n; s++; while (n > 0 && *s == CONTROL_TAG) { outc(lastput = ' '); --n; s++; } } else if (*s == (CONTROL_TAG | ((Uint32) '\e'))) { outc(lastput = '\e'); --n; ++s; } else if (*s & CONTROL_TAG) { outc('^'); outc(lastput = ((byte) ((*s == 0177) ? '?' : *s | 0x40))); n -= 2; s += 2; } else if (*s & ESCAPED_TAG) { Uint32 ch = *s & ~(TAG_MASK); byte *octbuff; byte octtmp[256]; int octbytes; DEBUGLOG(("write_buf: Escaped: %d", ch)); octbytes = octal_or_hex_positions(ch); if (octbytes > 256) { octbuff = driver_alloc(octbytes); } else { octbuff = octtmp; } octbytes = 0; octal_or_hex_format(ch, octbuff, &octbytes); DEBUGLOG(("write_buf: octbytes: %d", octbytes)); outc('\\'); for (i = 0; i < octbytes; ++i) { outc(lastput = octbuff[i]); DEBUGLOG(("write_buf: outc: %d", (int) lastput)); } n -= octbytes+1; s += octbytes+1; if (octbuff != octtmp) { driver_free(octbuff); } } else { DEBUGLOG(("write_buf: Very unexpected character %d",(int) *s)); ++n; --s; } } /* Check landed in first column of new line and have 'xn' bug. */ n = s - lbuf; if (xn && n != 0 && COL(cp_pos_to_col(n)) == 0) { if (n >= llen) { outc(' '); } else if (lastput == 0) { /* A multibyte UTF8 character */ for (i = 0; i < ubytes; ++i) { outc(ubuf[i]); } } else { outc(lastput); } move_left(1); } return TRUE; } /* The basic procedure for outputting one character. */ static int outc(int c) { putcbuf->orig_bytes[putcpos++] = c; if (putcpos == putclen) { driver_enq_bin(ttysl_port,putcbuf,0,putclen); driver_free_binary(putcbuf); putcpos = 0; putclen = TTY_BUFFSIZE; putcbuf = driver_alloc_binary(BUFSIZ); } return 1; } static int move_cursor(int from_pos, int to_pos) { int from_col, to_col; int dc, dl; update_cols(); from_col = cp_pos_to_col(from_pos); to_col = cp_pos_to_col(to_pos); dc = COL(to_col) - COL(from_col); dl = LINE(to_col) - LINE(from_col); DEBUGLOG(("move_cursor: from %d %d to %d %d => %d %d\n", from_pos, from_col, to_pos, to_col, dl, dc)); if (dl > 0) move_down(dl); else if (dl < 0) move_up(-dl); if (dc > 0) move_right(dc); else if (dc < 0) move_left(-dc); return to_col-from_col; } /* * Returns the length of an ANSI escape code in a buffer, this function only consider * color escape sequences like `\e[33m` or `\e[21;33m`. If a sequence has no valid * terminator, the length is equal the number of characters between `\e` and the first * invalid character, inclusive. */ static int ansi_escape_width(Uint32 *s, int max_length) { int i; if (*s != (CONTROL_TAG | ((Uint32) '\e'))) { return 0; } else if (max_length <= 1) { return 1; } else if (s[1] != '[') { return 2; } for (i = 2; i < max_length && (s[i] == ';' || (s[i] >= '0' && s[i] <= '9')); i++); return i + 1; } static int cp_pos_to_col(int cp_pos) { /* * If we don't have any character width information. Assume that * code points are one column wide */ int w = 1; int col = 0; int i = 0; int j; if (cp_pos > llen) { col += cp_pos - llen; cp_pos = llen; } while (i < cp_pos) { j = ansi_escape_width(lbuf + i, llen - i); if (j > 0) { i += j; } else { #ifdef HAVE_WCWIDTH w = wcwidth(lbuf[i]); #endif if (w > 0) { col += w; } i++; } } return col; } static int start_termcap(void) { int eres; size_t envsz = 1024; char *env = NULL; char *c; int tres; DEBUGLOG(("start_termcap: ..")); capbuf = driver_alloc(1024); if (!capbuf) goto false; eres = erl_drv_getenv("TERM", capbuf, &envsz); if (eres == 0) env = capbuf; else if (eres < 0) { DEBUGLOG(("start_termcap: failure in erl_drv_getenv(\"TERM\", ..) = %d\n", eres)); goto false; } else /* if (eres > 1) */ { char *envbuf = driver_alloc(envsz); if (!envbuf) goto false; while (1) { char *newenvbuf; eres = erl_drv_getenv("TERM", envbuf, &envsz); if (eres == 0) break; newenvbuf = driver_realloc(envbuf, envsz); if (eres < 0 || !newenvbuf) { DEBUGLOG(("start_termcap: failure in erl_drv_getenv(\"TERM\", ..) = %d or realloc buf == %p\n", eres, newenvbuf)); env = newenvbuf ? newenvbuf : envbuf; goto false; } envbuf = newenvbuf; } env = envbuf; } if ((tres = tgetent((char*)lbuf, env)) <= 0) { DEBUGLOG(("start_termcap: failure in tgetent(..) = %d\n", tres)); goto false; } if (env != capbuf) { env = NULL; driver_free(env); } c = capbuf; cols = tgetnum("co"); if (cols <= 0) cols = DEF_WIDTH; xn = tgetflag("xn"); up = tgetstr("up", &c); if (!(down = tgetstr("do", &c))) down = "\n"; if (!(left = tgetflag("bs") ? "\b" : tgetstr("bc", &c))) left = "\b"; /* Can't happen - but does on Solaris 2 */ right = tgetstr("nd", &c); if (up && down && left && right) { DEBUGLOG(("start_termcap: successful start\n")); return TRUE; } DEBUGLOG(("start_termcap: failed start\n")); false: if (env && env != capbuf) driver_free(env); if (capbuf) driver_free(capbuf); capbuf = NULL; return FALSE; } static int stop_termcap(void) { if (capbuf) driver_free(capbuf); capbuf = NULL; return TRUE; } static int move_left(int n) { while (n-- > 0) tputs(left, 1, outc); return TRUE; } static int move_right(int n) { while (n-- > 0) tputs(right, 1, outc); return TRUE; } static int move_up(int n) { while (n-- > 0) tputs(up, 1, outc); return TRUE; } static int move_down(int n) { while (n-- > 0) tputs(down, 1, outc); return TRUE; } /* * Updates cols if terminal has resized (SIGWINCH). Should be called * at the start of any function that uses the COL or LINE macros. If * the terminal is resized after calling this function but before use * of the macros, then we may write to the wrong screen location. * * We cannot call this from the SIGWINCH handler because it uses * ioctl() which is not a safe function as listed in the signal(7) * man page. */ static void update_cols(void) { Uint32 width, height; if (cols_needs_update) { cols_needs_update = FALSE; ttysl_get_window_size(&width, &height); cols = width; } } /* * Put a terminal device into non-canonical mode with ECHO off. * Before doing so we first save the terminal's current mode, * assuming the caller will call the tty_reset() function * (also in this file) when it's done with raw mode. */ static struct termios tty_smode, tty_rmode; static int tty_init(int fd, int canon, int echo, int sig) { int tres; DEBUGLOG(("tty_init: fd = %d, canon = %d, echo = %d, sig = %d", fd, canon, echo, sig)); if ((tres = tcgetattr(fd, &tty_rmode)) < 0) { DEBUGLOG(("tty_init: failure in tcgetattr(%d,..) = %d\n", fd, tres)); return -1; } tty_smode = tty_rmode; /* Default characteristics for all usage including termcap output. */ tty_smode.c_iflag &= ~ISTRIP; /* Turn canonical (line mode) on off. */ if (canon > 0) { tty_smode.c_iflag |= ICRNL; tty_smode.c_lflag |= ICANON; tty_smode.c_oflag |= OPOST; tty_smode.c_cc[VEOF] = tty_rmode.c_cc[VEOF]; #ifdef VDSUSP tty_smode.c_cc[VDSUSP] = tty_rmode.c_cc[VDSUSP]; #endif } if (canon < 0) { tty_smode.c_iflag &= ~ICRNL; tty_smode.c_lflag &= ~ICANON; tty_smode.c_oflag &= ~OPOST; /* Must get these really right or funny effects can occur. */ tty_smode.c_cc[VMIN] = 1; tty_smode.c_cc[VTIME] = 0; #ifdef VDSUSP tty_smode.c_cc[VDSUSP] = 0; #endif } /* Turn echo on or off. */ if (echo > 0) tty_smode.c_lflag |= ECHO; if (echo < 0) tty_smode.c_lflag &= ~ECHO; /* Set extra characteristics for "RAW" mode, no signals. */ if (sig > 0) { /* Ignore IMAXBEL as not POSIX. */ #ifndef QNX tty_smode.c_iflag |= (BRKINT|IGNPAR|ICRNL|IXON|IXANY); #else tty_smode.c_iflag |= (BRKINT|IGNPAR|ICRNL|IXON); #endif tty_smode.c_lflag |= (ISIG|IEXTEN); } if (sig < 0) { /* Ignore IMAXBEL as not POSIX. */ #ifndef QNX tty_smode.c_iflag &= ~(BRKINT|IGNPAR|ICRNL|IXON|IXANY); #else tty_smode.c_iflag &= ~(BRKINT|IGNPAR|ICRNL|IXON); #endif tty_smode.c_lflag &= ~(ISIG|IEXTEN); } DEBUGLOG(("tty_init: successful init\n")); return 0; } /* * Set/restore a terminal's mode to whatever it was on the most * recent call to the tty_init() function above. */ static int tty_set(int fd) { int tres; DEBUGF(("tty_set: Setting tty...\n")); if ((tres = tcsetattr(fd, TCSANOW, &tty_smode)) < 0) { DEBUGLOG(("tty_set: failure in tcgetattr(%d,..) = %d\n", fd, tres)); return(-1); } return(0); } static int tty_reset(int fd) /* of terminal device */ { int tres; DEBUGF(("tty_reset: Resetting tty...\n")); if ((tres = tcsetattr(fd, TCSANOW, &tty_rmode)) < 0) { DEBUGLOG(("tty_reset: failure in tcsetattr(%d,..) = %d\n", fd, tres)); return(-1); } return(0); } /* * Signal handler to cope with signals so that we can reset the tty * to the orignal settings */ #ifdef ERTS_NOT_USED /* XXX: A mistake that it isn't used, or should it be removed? */ static RETSIGTYPE suspend(int sig) { if (tty_reset(ttysl_fd) < 0) { DEBUGLOG(("signal: failure in suspend(%d), can't reset tty %d\n", sig, ttysl_fd)); fprintf(stderr,"Can't reset tty \n"); exit(1); } sys_signal(sig, SIG_DFL); /* Set signal handler to default */ sys_sigrelease(sig); /* Allow 'sig' to come through */ kill(getpid(), sig); /* Send ourselves the signal */ sys_sigblock(sig); /* Reset to old mask */ sys_signal(sig, suspend); /* Reset signal handler */ if (tty_set(ttysl_fd) < 0) { DEBUGLOG(("signal: failure in suspend(%d), can't set tty %d\n", sig, ttysl_fd)); fprintf(stderr,"Can't set tty raw \n"); exit(1); } } #endif static RETSIGTYPE cont(int sig) { if (tty_set(ttysl_fd) < 0) { DEBUGLOG(("signal: failure in cont(%d), can't set tty raw %d\n", sig, ttysl_fd)); fprintf(stderr,"Can't set tty raw\n"); exit(1); } } static RETSIGTYPE winch(int sig) { cols_needs_update = TRUE; } #endif /* HAVE_TERMCAP */