From beab6a3c022f39b0808235d00dd664125a3dfc06 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 31 Mar 2021 17:28:26 -0400 Subject: [PATCH] Migrate VT buffering to c C implementation to improve memory and cpu utilization. Forked off to further move the work off the main process. Still needs attribute rendition and packaging before merging to main branch. --- confluent_server/confluent/consoleserver.py | 187 +++---- confluent_vtbufferd/Makefile | 5 + confluent_vtbufferd/tmt.c | 521 ++++++++++++++++++++ confluent_vtbufferd/tmt.h | 140 ++++++ confluent_vtbufferd/vtbufferd.c | 143 ++++++ 5 files changed, 869 insertions(+), 127 deletions(-) create mode 100644 confluent_vtbufferd/Makefile create mode 100644 confluent_vtbufferd/tmt.c create mode 100644 confluent_vtbufferd/tmt.h create mode 100644 confluent_vtbufferd/vtbufferd.c diff --git a/confluent_server/confluent/consoleserver.py b/confluent_server/confluent/consoleserver.py index 9f996a58..ee4e6cd8 100644 --- a/confluent_server/confluent/consoleserver.py +++ b/confluent_server/confluent/consoleserver.py @@ -33,42 +33,67 @@ import confluent.tlvdata as tlvdata import confluent.util as util import eventlet import eventlet.event +import eventlet.green.os as os +import eventlet.green.select as select import eventlet.green.socket as socket +import eventlet.green.subprocess as subprocess import eventlet.green.ssl as ssl -import pyte +import eventlet.semaphore as semaphore +import fcntl import random +import struct import time import traceback _handled_consoles = {} _tracelog = None +_bufferdaemon = None +_bufferlock = None try: range = xrange except NameError: pass -pytecolors2ansi = { - 'black': 0, - 'red': 1, - 'green': 2, - 'brown': 3, - 'blue': 4, - 'magenta': 5, - 'cyan': 6, - 'white': 7, - 'default': 9, -} -# might be able to use IBMPC map from pyte charsets, -# in that case, would have to mask out certain things (like ESC) -# in the same way that Screen's draw method would do -# for now at least get some of the arrows in there (note ESC is one -# of those arrows... so skip it... -ansichars = dict(zip((0x18, 0x19), u'\u2191\u2193')) + +def chunk_output(output, n): + for i in range(0, len(output), n): + yield output[i:i + n] + +def get_buffer_output(nodename): + out = _bufferdaemon.stdin + instream = _bufferdaemon.stdout + if not isinstance(nodename, bytes): + nodename = nodename.encode('utf8') + outdata = b'' + with _bufferlock: + out.write(struct.pack('I', len(nodename))) + out.write(nodename) + out.flush() + select.select((instream,), (), (), 30) + while not outdata or outdata[-1]: + chunk = instream.read(128) + if chunk: + outdata += chunk + else: + select.select((instream,), (), (), 0) + return outdata[:-1] -def _utf8_normalize(data, shiftin, decoder): +def send_output(nodename, output): + if not isinstance(nodename, bytes): + nodename = nodename.encode('utf8') + with _bufferlock: + _bufferdaemon.stdin.write(struct.pack('I', len(nodename) | (1 << 29))) + _bufferdaemon.stdin.write(nodename) + _bufferdaemon.stdin.flush() + for chunk in chunk_output(output, 8192): + _bufferdaemon.stdin.write(struct.pack('I', len(chunk) | (2 << 29))) + _bufferdaemon.stdin.write(chunk) + _bufferdaemon.stdin.flush() + +def _utf8_normalize(data, decoder): # first we give the stateful decoder a crack at the byte stream, # we may come up empty in the event of a partial multibyte try: @@ -88,60 +113,9 @@ def _utf8_normalize(data, shiftin, decoder): # Finally, the low part of ascii is valid utf-8, but we are going to be # more interested in the cp437 versions (since this is console *output* # not input - if shiftin is None: - data = data.translate(ansichars) return data.encode('utf-8') -def pytechars2line(chars, maxlen=None): - line = b'\x1b[m' # start at default params - lb = False # last bold - li = False # last italic - lu = False # last underline - ls = False # last strikethrough - lr = False # last reverse - lfg = 'default' # last fg color - lbg = 'default' # last bg color - hasdata = False - len = 1 - for charidx in range(maxlen): - char = chars[charidx] - csi = bytearray([]) - if char.fg != lfg: - csi.append(30 + pytecolors2ansi.get(char.fg, 9)) - lfg = char.fg - if char.bg != lbg: - csi.append(40 + pytecolors2ansi.get(char.bg, 9)) - lbg = char.bg - if char.bold != lb: - lb = char.bold - csi.append(1 if lb else 22) - if char.italics != li: - li = char.italics - csi.append(3 if li else 23) - if char.underscore != lu: - lu = char.underscore - csi.append(4 if lu else 24) - if char.strikethrough != ls: - ls = char.strikethrough - csi.append(9 if ls else 29) - if char.reverse != lr: - lr = char.reverse - csi.append(7 if lr else 27) - if csi: - line += b'\x1b[' + b';'.join(['{0}'.format(x).encode('utf-8') for x in csi]) + b'm' - if not hasdata and char.data.rstrip(): - hasdata = True - chardata = char.data - if not isinstance(chardata, bytes): - chardata = chardata.encode('utf-8') - line += chardata - if maxlen and len >= maxlen: - break - len += 1 - return line, hasdata - - class ConsoleHandler(object): _plugin_path = '/nodes/{0}/_console/session' _logtobuffer = True @@ -161,15 +135,15 @@ class ConsoleHandler(object): self.node = node self.connectstate = 'unconnected' self._isalive = True - self.buffer = pyte.Screen(100, 31) - self.termstream = pyte.ByteStream() - self.termstream.attach(self.buffer) + #self.buffer = pyte.Screen(100, 31) + #self.termstream = pyte.ByteStream() + #self.termstream.attach(self.buffer) self.livesessions = set([]) self.utf8decoder = codecs.getincrementaldecoder('utf-8')() if self._logtobuffer: self.logger = log.Logger(node, console=True, tenant=configmanager.tenant) - (text, termstate, timestamp) = (b'', 0, False) + timestamp = False # when reading from log file, we will use wall clock # it should usually match walltime. self.lasttime = 0 @@ -182,13 +156,7 @@ class ConsoleHandler(object): # guess self.lasttime = util.monotonic_time() self.clearbuffer() - self.appmodedetected = False - self.shiftin = None self.reconnect = None - if termstate & 1: - self.appmodedetected = True - if termstate & 2: - self.shiftin = b'0' self.users = {} self._attribwatcher = None self._console = None @@ -216,10 +184,7 @@ class ConsoleHandler(object): if not isinstance(data, bytes): data = data.encode('utf-8') try: - self.termstream.feed(data) - except StopIteration: # corrupt parser state, start over - self.termstream = pyte.ByteStream() - self.termstream.attach(self.buffer) + send_output(self.node, data) except Exception: _tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event, event=log.Events.stacktrace) @@ -536,19 +501,7 @@ class ConsoleHandler(object): return if not isinstance(data, bytes): data = data.encode('utf-8') - if b'\x1b[?1l' in data: # request for ansi mode cursor keys - self.appmodedetected = False - if b'\x1b[?1h' in data: # remember the session wants the client to use - # 'application mode' Thus far only observed on esxi - self.appmodedetected = True - if b'\x1b)0' in data: - # console indicates it wants access to special drawing characters - self.shiftin = b'0' eventdata = 0 - if self.appmodedetected: - eventdata |= 1 - if self.shiftin is not None: - eventdata |= 2 # TODO: analyze buffer for registered events, examples: # panics # certificate signing request @@ -557,7 +510,7 @@ class ConsoleHandler(object): self.clearerror = False self.feedbuffer(b'\x1bc\x1b[2J\x1b[1;1H') self._send_rcpts(b'\x1bc\x1b[2J\x1b[1;1H') - self._send_rcpts(_utf8_normalize(data, self.shiftin, self.utf8decoder)) + self._send_rcpts(_utf8_normalize(data, self.utf8decoder)) self.log(data, eventdata=eventdata) self.lasttime = util.monotonic_time() self.feedbuffer(data) @@ -583,36 +536,7 @@ class ConsoleHandler(object): 'connectstate': self.connectstate, 'clientcount': len(self.livesessions), } - retdata = b'\x1b[H\x1b[J' # clear screen - pendingbl = b'' # pending blank lines - maxlen = 0 - for line in self.buffer.display: - line = line.rstrip() - if len(line) > maxlen: - maxlen = len(line) - for line in range(self.buffer.lines): - nline, notblank = pytechars2line(self.buffer.buffer[line], maxlen) - if notblank: - if pendingbl: - retdata += pendingbl - pendingbl = b'' - retdata += nline + b'\r\n' - else: - pendingbl += nline + b'\r\n' - if len(retdata) > 6: - retdata = retdata[:-2] # remove the last \r\n - cursordata = '\x1b[{0};{1}H'.format(self.buffer.cursor.y + 1, - self.buffer.cursor.x + 1) - if not isinstance(cursordata, bytes): - cursordata = cursordata.encode('utf-8') - retdata += cursordata - if self.shiftin is not None: # detected that terminal requested a - # shiftin character set, relay that to the terminal that cannected - retdata += b'\x1b)' + self.shiftin - #if self.appmodedetected: - # retdata += b'\x1b[?1h' - #else: - # retdata += b'\x1b[?1l' + retdata = get_buffer_output(self.node) return retdata, connstate def write(self, data): @@ -661,7 +585,16 @@ def _start_tenant_sessions(cfm): def start_console_sessions(): global _tracelog + global _bufferdaemon + global _bufferlock + _bufferlock = semaphore.Semaphore() _tracelog = log.Logger('trace') + _bufferdaemon = subprocess.Popen( + ['/opt/confluent/bin/vtbufferd'], stdin=subprocess.PIPE, + stdout=subprocess.PIPE) + fl = fcntl.fcntl(_bufferdaemon.stdout.fileno(), fcntl.F_GETFL) + fcntl.fcntl(_bufferdaemon.stdout.fileno(), + fcntl.F_SETFL, fl | os.O_NONBLOCK) configmodule.hook_new_configmanagers(_start_tenant_sessions) diff --git a/confluent_vtbufferd/Makefile b/confluent_vtbufferd/Makefile new file mode 100644 index 00000000..328ba1f9 --- /dev/null +++ b/confluent_vtbufferd/Makefile @@ -0,0 +1,5 @@ +vtbufferd: + gcc -O3 -o vtbufferd vtbufferd.c tmt.c + +clean: + rm vtbufferd diff --git a/confluent_vtbufferd/tmt.c b/confluent_vtbufferd/tmt.c new file mode 100644 index 00000000..19569281 --- /dev/null +++ b/confluent_vtbufferd/tmt.c @@ -0,0 +1,521 @@ +/* Copyright (c) 2017 Rob King + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the copyright holder nor the + * names of contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS, + * COPYRIGHT HOLDERS, OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +#include +#include +#include +#include +#include +#include "tmt.h" + +#define BUF_MAX 100 +#define PAR_MAX 8 +#define TAB 8 +#define MAX(x, y) (((size_t)(x) > (size_t)(y)) ? (size_t)(x) : (size_t)(y)) +#define MIN(x, y) (((size_t)(x) < (size_t)(y)) ? (size_t)(x) : (size_t)(y)) +#define CLINE(vt) (vt)->screen.lines[MIN((vt)->curs.r, (vt)->screen.nline - 1)] + +#define P0(x) (vt->pars[x]) +#define P1(x) (vt->pars[x]? vt->pars[x] : 1) +#define CB(vt, m, a) ((vt)->cb? (vt)->cb(m, vt, a, (vt)->p) : (void)0) +#define INESC ((vt)->state) + +#define COMMON_VARS \ + TMTSCREEN *s = &vt->screen; \ + TMTPOINT *c = &vt->curs; \ + TMTLINE *l = CLINE(vt); \ + TMTCHAR *t = vt->tabs->chars + +#define HANDLER(name) static void name (TMT *vt) { COMMON_VARS; + +struct TMT{ + TMTPOINT curs, oldcurs; + TMTATTRS attrs, oldattrs; + + bool dirty, acs, decdraw, ignored; + TMTSCREEN screen; + TMTLINE *tabs; + + TMTCALLBACK cb; + void *p; + const wchar_t *acschars; + const wchar_t *decchars; + + mbstate_t ms; + size_t nmb; + char mb[BUF_MAX + 1]; + + size_t pars[PAR_MAX]; + size_t npar; + size_t arg; + enum {S_NUL, S_ESC, S_ARG, S_SCS} state; +}; + +static TMTATTRS defattrs = {.fg = TMT_COLOR_DEFAULT, .bg = TMT_COLOR_DEFAULT}; +static void writecharatcurs(TMT *vt, wchar_t w); + +static wchar_t +decchar(const TMT *vt, unsigned char c) +{ + if (c > 94 && c < 127) + return vt->decchars[c - 95]; + return (wchar_t)c; +} + +static wchar_t +tacs(const TMT *vt, unsigned char c) +{ + /* The terminfo alternate character set for ANSI. */ + static unsigned char map[] = {0020U, 0021U, 0030U, 0031U, 0333U, 0004U, + 0261U, 0370U, 0361U, 0260U, 0331U, 0277U, + 0332U, 0300U, 0305U, 0176U, 0304U, 0304U, + 0304U, 0137U, 0303U, 0264U, 0301U, 0302U, + 0263U, 0363U, 0362U, 0343U, 0330U, 0234U, + 0376U}; + for (size_t i = 0; i < sizeof(map); i++) if (map[i] == c) + return vt->acschars[i]; + return (wchar_t)c; +} + +static void +dirtylines(TMT *vt, size_t s, size_t e) +{ + vt->dirty = true; + for (size_t i = s; i < e; i++) + vt->screen.lines[i]->dirty = true; +} + +static void +clearline(TMT *vt, TMTLINE *l, size_t s, size_t e) +{ + vt->dirty = l->dirty = true; + for (size_t i = s; i < e && i < vt->screen.ncol; i++){ + l->chars[i].a = defattrs; + l->chars[i].c = L' '; + } +} + +static void +clearlines(TMT *vt, size_t r, size_t n) +{ + for (size_t i = r; i < r + n && i < vt->screen.nline; i++) + clearline(vt, vt->screen.lines[i], 0, vt->screen.ncol); +} + +static void +scrup(TMT *vt, size_t r, size_t n) +{ + n = MIN(n, vt->screen.nline - 1 - r); + + if (n){ + TMTLINE *buf[n]; + + memcpy(buf, vt->screen.lines + r, n * sizeof(TMTLINE *)); + memmove(vt->screen.lines + r, vt->screen.lines + r + n, + (vt->screen.nline - n - r) * sizeof(TMTLINE *)); + memcpy(vt->screen.lines + (vt->screen.nline - n), + buf, n * sizeof(TMTLINE *)); + + clearlines(vt, vt->screen.nline - n, n); + dirtylines(vt, r, vt->screen.nline); + } +} + +static void +scrdn(TMT *vt, size_t r, size_t n) +{ + n = MIN(n, vt->screen.nline - 1 - r); + + if (n){ + TMTLINE *buf[n]; + + memcpy(buf, vt->screen.lines + (vt->screen.nline - n), + n * sizeof(TMTLINE *)); + memmove(vt->screen.lines + r + n, vt->screen.lines + r, + (vt->screen.nline - n - r) * sizeof(TMTLINE *)); + memcpy(vt->screen.lines + r, buf, n * sizeof(TMTLINE *)); + + clearlines(vt, r, n); + dirtylines(vt, r, vt->screen.nline); + } +} + +HANDLER(ed) + size_t b = 0; + size_t e = s->nline; + + switch (P0(0)){ + case 0: b = c->r + 1; clearline(vt, l, c->c, vt->screen.ncol); break; + case 1: e = c->r - 1; clearline(vt, l, 0, c->c); break; + case 2: /* use defaults */ break; + default: /* do nothing */ return; + } + + clearlines(vt, b, e - b); +} + +HANDLER(ich) + size_t n = P1(0); /* XXX use MAX */ + if (n > s->ncol - c->c - 1) n = s->ncol - c->c - 1; + + memmove(l->chars + c->c + n, l->chars + c->c, + MIN(s->ncol - 1 - c->c, + (s->ncol - c->c - n - 1)) * sizeof(TMTCHAR)); + clearline(vt, l, c->c, n); +} + +HANDLER(dch) + size_t n = P1(0); /* XXX use MAX */ + if (n > s->ncol - c->c) n = s->ncol - c->c; + else if (n == 0) return; + + memmove(l->chars + c->c, l->chars + c->c + n, + (s->ncol - c->c - n) * sizeof(TMTCHAR)); + + clearline(vt, l, s->ncol - n, s->ncol); + /* VT102 manual says the attribute for the newly empty characters + * should be the same as the last character moved left, which isn't + * what clearline() currently does. + */ +} + +HANDLER(el) + switch (P0(0)){ + case 0: clearline(vt, l, c->c, vt->screen.ncol); break; + case 1: clearline(vt, l, 0, MIN(c->c + 1, s->ncol - 1)); break; + case 2: clearline(vt, l, 0, vt->screen.ncol); break; + } +} + +HANDLER(sgr) + #define FGBG(c) *(P0(i) < 40? &vt->attrs.fg : &vt->attrs.bg) = c + for (size_t i = 0; i < vt->npar; i++) switch (P0(i)){ + case 0: vt->attrs = defattrs; break; + case 1: case 22: vt->attrs.bold = P0(0) < 20; break; + case 2: case 23: vt->attrs.dim = P0(0) < 20; break; + case 4: case 24: vt->attrs.underline = P0(0) < 20; break; + case 5: case 25: vt->attrs.blink = P0(0) < 20; break; + case 7: case 27: vt->attrs.reverse = P0(0) < 20; break; + case 8: case 28: vt->attrs.invisible = P0(0) < 20; break; + case 10: case 11: vt->acs = P0(0) > 10; break; + case 30: case 40: FGBG(TMT_COLOR_BLACK); break; + case 31: case 41: FGBG(TMT_COLOR_RED); break; + case 32: case 42: FGBG(TMT_COLOR_GREEN); break; + case 33: case 43: FGBG(TMT_COLOR_YELLOW); break; + case 34: case 44: FGBG(TMT_COLOR_BLUE); break; + case 35: case 45: FGBG(TMT_COLOR_MAGENTA); break; + case 36: case 46: FGBG(TMT_COLOR_CYAN); break; + case 37: case 47: FGBG(TMT_COLOR_WHITE); break; + case 39: case 49: FGBG(TMT_COLOR_DEFAULT); break; + } +} + +HANDLER(rep) + if (!c->c) return; + wchar_t r = l->chars[c->c - 1].c; + for (size_t i = 0; i < P1(0); i++) + writecharatcurs(vt, r); +} + +HANDLER(dsr) + char r[BUF_MAX + 1] = {0}; + snprintf(r, BUF_MAX, "\033[%zd;%zdR", c->r + 1, c->c + 1); + CB(vt, TMT_MSG_ANSWER, (const char *)r); +} + +HANDLER(resetparser) + memset(vt->pars, 0, sizeof(vt->pars)); + vt->state = vt->npar = vt->arg = vt->ignored = (bool)0; +} + +HANDLER(consumearg) + if (vt->npar < PAR_MAX) + vt->pars[vt->npar++] = vt->arg; + vt->arg = 0; +} + +HANDLER(fixcursor) + c->r = MIN(c->r, s->nline - 1); + c->c = MIN(c->c, s->ncol - 1); +} + +static bool +handlechar(TMT *vt, char i) +{ + COMMON_VARS; + + char cs[] = {i, 0}; + #define ON(S, C, A) if (vt->state == (S) && strchr(C, i)){ A; return true;} + #define DO(S, C, A) ON(S, C, consumearg(vt); if (!vt->ignored) {A;} \ + fixcursor(vt); resetparser(vt);); + + DO(S_NUL, "\x07", CB(vt, TMT_MSG_BELL, NULL)) + DO(S_NUL, "\x08", if (c->c) c->c--) + DO(S_NUL, "\x09", while (++c->c < s->ncol - 1 && t[c->c].c != L'*')) + DO(S_NUL, "\x0a", c->r < s->nline - 1? (void)c->r++ : scrup(vt, 0, 1)) + DO(S_NUL, "\x0d", c->c = 0) + ON(S_NUL, "\x1b", vt->state = S_ESC) + ON(S_ESC, "\x1b", vt->state = S_ESC) + DO(S_ESC, "H", t[c->c].c = L'*') + DO(S_ESC, "7", vt->oldcurs = vt->curs; vt->oldattrs = vt->attrs) + DO(S_ESC, "8", vt->curs = vt->oldcurs; vt->attrs = vt->oldattrs) + ON(S_ESC, "+*)", vt->ignored = true; vt->state = S_ARG) + ON(S_ESC, "(", vt->state = S_SCS) + DO(S_SCS, "0", vt->decdraw = true) + DO(S_SCS, "B", vt->decdraw = false) + DO(S_ESC, "c", tmt_reset(vt)) + DO(S_ESC, "M", if (c->r) c->r--) + ON(S_ESC, "[", vt->state = S_ARG) + ON(S_ARG, "\x1b", vt->state = S_ESC) + ON(S_ARG, ";", consumearg(vt)) + ON(S_ARG, "?", (void)0) + ON(S_ARG, "0123456789", vt->arg = vt->arg * 10 + atoi(cs)) + DO(S_ARG, "A", c->r = MAX(c->r - P1(0), 0)) + DO(S_ARG, "B", c->r = MIN(c->r + P1(0), s->nline - 1)) + DO(S_ARG, "C", c->c = MIN(c->c + P1(0), s->ncol - 1)) + DO(S_ARG, "D", c->c = MIN(c->c - P1(0), c->c)) + DO(S_ARG, "E", c->c = 0; c->r = MIN(c->r + P1(0), s->nline - 1)) + DO(S_ARG, "F", c->c = 0; c->r = MAX(c->r - P1(0), 0)) + DO(S_ARG, "G", c->c = MIN(P1(0) - 1, s->ncol - 1)) + DO(S_ARG, "d", c->r = MIN(P1(0) - 1, s->nline - 1)) + DO(S_ARG, "Hf", c->r = P1(0) - 1; c->c = P1(1) - 1) + DO(S_ARG, "I", while (++c->c < s->ncol - 1 && t[c->c].c != L'*')) + DO(S_ARG, "J", ed(vt)) + DO(S_ARG, "K", el(vt)) + DO(S_ARG, "L", scrdn(vt, c->r, P1(0))) + DO(S_ARG, "M", scrup(vt, c->r, P1(0))) + DO(S_ARG, "P", dch(vt)) + DO(S_ARG, "S", scrup(vt, 0, P1(0))) + DO(S_ARG, "T", scrdn(vt, 0, P1(0))) + DO(S_ARG, "X", clearline(vt, l, c->c, P1(0))) + DO(S_ARG, "Z", while (c->c && t[--c->c].c != L'*')) + DO(S_ARG, "b", rep(vt)); + DO(S_ARG, "c", CB(vt, TMT_MSG_ANSWER, "\033[?6c")) + DO(S_ARG, "g", if (P0(0) == 3) clearline(vt, vt->tabs, 0, s->ncol)) + DO(S_ARG, "m", sgr(vt)) + DO(S_ARG, "n", if (P0(0) == 6) dsr(vt)) + DO(S_ARG, "h", if (P0(0) == 25) CB(vt, TMT_MSG_CURSOR, "t")) + DO(S_ARG, "i", (void)0) + DO(S_ARG, "l", if (P0(0) == 25) CB(vt, TMT_MSG_CURSOR, "f")) + DO(S_ARG, "s", vt->oldcurs = vt->curs; vt->oldattrs = vt->attrs) + DO(S_ARG, "u", vt->curs = vt->oldcurs; vt->attrs = vt->oldattrs) + DO(S_ARG, "@", ich(vt)) + + return resetparser(vt), false; +} + +static void +notify(TMT *vt, bool update, bool moved) +{ + if (update) CB(vt, TMT_MSG_UPDATE, &vt->screen); + if (moved) CB(vt, TMT_MSG_MOVED, &vt->curs); +} + +static TMTLINE * +allocline(TMT *vt, TMTLINE *o, size_t n, size_t pc) +{ + TMTLINE *l = realloc(o, sizeof(TMTLINE) + n * sizeof(TMTCHAR)); + if (!l) return NULL; + + clearline(vt, l, pc, n); + return l; +} + +static void +freelines(TMT *vt, size_t s, size_t n, bool screen) +{ + for (size_t i = s; vt->screen.lines && i < s + n; i++){ + free(vt->screen.lines[i]); + vt->screen.lines[i] = NULL; + } + if (screen) free(vt->screen.lines); +} + +TMT * +tmt_open(size_t nline, size_t ncol, TMTCALLBACK cb, void *p, + const wchar_t *acs) +{ + TMT *vt = calloc(1, sizeof(TMT)); + if (!nline || !ncol || !vt) return free(vt), NULL; + + /* ASCII-safe defaults for box-drawing characters. */ + vt->acschars = acs? acs : L"→←↑↓■◆▒°±▒┘┐┌└┼⎺───⎽├┤┴┬│≤≥π≠£•"; //L"><^v#+:o##+++++~---_++++|<>*!fo"; + vt->decchars = L" ◆▒\t\f\r\n°±\n\v┘┐┌└┼⎺⎻─⎼⎽├┤┴┬│≤≥π≠£•"; + vt->cb = cb; + vt->p = p; + + if (!tmt_resize(vt, nline, ncol)) return tmt_close(vt), NULL; + return vt; +} + +void +tmt_close(TMT *vt) +{ + free(vt->tabs); + freelines(vt, 0, vt->screen.nline, true); + free(vt); +} + +bool +tmt_resize(TMT *vt, size_t nline, size_t ncol) +{ + if (nline < 2 || ncol < 2) return false; + if (nline < vt->screen.nline) + freelines(vt, nline, vt->screen.nline - nline, false); + + TMTLINE **l = realloc(vt->screen.lines, nline * sizeof(TMTLINE *)); + if (!l) return false; + + size_t pc = vt->screen.ncol; + vt->screen.lines = l; + vt->screen.ncol = ncol; + for (size_t i = 0; i < nline; i++){ + TMTLINE *nl = NULL; + if (i >= vt->screen.nline) + nl = vt->screen.lines[i] = allocline(vt, NULL, ncol, 0); + else + nl = allocline(vt, vt->screen.lines[i], ncol, pc); + + if (!nl) return false; + vt->screen.lines[i] = nl; + } + vt->screen.nline = nline; + + vt->tabs = allocline(vt, vt->tabs, ncol, 0); + if (!vt->tabs) return free(l), false; + vt->tabs->chars[0].c = vt->tabs->chars[ncol - 1].c = L'*'; + for (size_t i = 0; i < ncol; i++) if (i % TAB == 0) + vt->tabs->chars[i].c = L'*'; + + fixcursor(vt); + dirtylines(vt, 0, nline); + notify(vt, true, true); + return true; +} + +static void +writecharatcurs(TMT *vt, wchar_t w) +{ + COMMON_VARS; + + #ifdef TMT_HAS_WCWIDTH + extern int wcwidth(wchar_t c); + if (wcwidth(w) > 1) w = TMT_INVALID_CHAR; + if (wcwidth(w) < 0) return; + #endif + + CLINE(vt)->chars[vt->curs.c].c = w; + CLINE(vt)->chars[vt->curs.c].a = vt->attrs; + CLINE(vt)->dirty = vt->dirty = true; + + if (c->c < s->ncol - 1) + c->c++; + else{ + c->c = 0; + c->r++; + } + + if (c->r >= s->nline){ + c->r = s->nline - 1; + scrup(vt, 0, 1); + } +} + +static inline size_t +testmbchar(TMT *vt) +{ + mbstate_t ts = vt->ms; + return vt->nmb? mbrtowc(NULL, vt->mb, vt->nmb, &ts) : (size_t)-2; +} + +static inline wchar_t +getmbchar(TMT *vt) +{ + wchar_t c = 0; + size_t n = mbrtowc(&c, vt->mb, vt->nmb, &vt->ms); + vt->nmb = 0; + return (n == (size_t)-1 || n == (size_t)-2)? TMT_INVALID_CHAR : c; +} + +void +tmt_write(TMT *vt, const char *s, size_t n) +{ + TMTPOINT oc = vt->curs; + n = n? n : strlen(s); + + for (size_t p = 0; p < n; p++){ + if (handlechar(vt, s[p])) + continue; + else if (vt->acs) + writecharatcurs(vt, tacs(vt, (unsigned char)s[p])); + else if (vt->decdraw) + writecharatcurs(vt, decchar(vt, (unsigned char)s[p])); + else if (vt->nmb >= BUF_MAX) + writecharatcurs(vt, getmbchar(vt)); + else{ + switch (testmbchar(vt)){ + case (size_t)-1: writecharatcurs(vt, getmbchar(vt)); break; + case (size_t)-2: vt->mb[vt->nmb++] = s[p]; break; + } + + if (testmbchar(vt) <= MB_LEN_MAX) + writecharatcurs(vt, getmbchar(vt)); + } + } + + notify(vt, vt->dirty, memcmp(&oc, &vt->curs, sizeof(oc)) != 0); +} + +const TMTSCREEN * +tmt_screen(const TMT *vt) +{ + return &vt->screen; +} + +const TMTPOINT * +tmt_cursor(const TMT *vt) +{ + return &vt->curs; +} + +void +tmt_clean(TMT *vt) +{ + for (size_t i = 0; i < vt->screen.nline; i++) + vt->dirty = vt->screen.lines[i]->dirty = false; +} + +void +tmt_reset(TMT *vt) +{ + vt->curs.r = vt->curs.c = vt->oldcurs.r = vt->oldcurs.c = vt->acs = (bool)0; + resetparser(vt); + vt->attrs = vt->oldattrs = defattrs; + memset(&vt->ms, 0, sizeof(vt->ms)); + clearlines(vt, 0, vt->screen.nline); + CB(vt, TMT_MSG_CURSOR, "t"); + notify(vt, true, true); +} diff --git a/confluent_vtbufferd/tmt.h b/confluent_vtbufferd/tmt.h new file mode 100644 index 00000000..ae0ddbb9 --- /dev/null +++ b/confluent_vtbufferd/tmt.h @@ -0,0 +1,140 @@ +/* Copyright (c) 2017 Rob King + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the copyright holder nor the + * names of contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS, + * COPYRIGHT HOLDERS, OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef TMT_H +#define TMT_H + +#include +#include +#include + +/**** INVALID WIDE CHARACTER */ +#ifndef TMT_INVALID_CHAR +#define TMT_INVALID_CHAR ((wchar_t)0xfffd) +#endif + +/**** INPUT SEQUENCES */ +#define TMT_KEY_UP "\033[A" +#define TMT_KEY_DOWN "\033[B" +#define TMT_KEY_RIGHT "\033[C" +#define TMT_KEY_LEFT "\033[D" +#define TMT_KEY_HOME "\033[H" +#define TMT_KEY_END "\033[Y" +#define TMT_KEY_INSERT "\033[L" +#define TMT_KEY_BACKSPACE "\x08" +#define TMT_KEY_ESCAPE "\x1b" +#define TMT_KEY_BACK_TAB "\033[Z" +#define TMT_KEY_PAGE_UP "\033[V" +#define TMT_KEY_PAGE_DOWN "\033[U" +#define TMT_KEY_F1 "\033OP" +#define TMT_KEY_F2 "\033OQ" +#define TMT_KEY_F3 "\033OR" +#define TMT_KEY_F4 "\033OS" +#define TMT_KEY_F5 "\033OT" +#define TMT_KEY_F6 "\033OU" +#define TMT_KEY_F7 "\033OV" +#define TMT_KEY_F8 "\033OW" +#define TMT_KEY_F9 "\033OX" +#define TMT_KEY_F10 "\033OY" + +/**** BASIC DATA STRUCTURES */ +typedef struct TMT TMT; + +typedef enum{ + TMT_COLOR_DEFAULT = -1, + TMT_COLOR_BLACK = 1, + TMT_COLOR_RED, + TMT_COLOR_GREEN, + TMT_COLOR_YELLOW, + TMT_COLOR_BLUE, + TMT_COLOR_MAGENTA, + TMT_COLOR_CYAN, + TMT_COLOR_WHITE, + TMT_COLOR_MAX +} tmt_color_t; + +typedef struct TMTATTRS TMTATTRS; +struct TMTATTRS{ + bool bold; + bool dim; + bool underline; + bool blink; + bool reverse; + bool invisible; + tmt_color_t fg; + tmt_color_t bg; +}; + +typedef struct TMTCHAR TMTCHAR; +struct TMTCHAR{ + wchar_t c; + TMTATTRS a; +}; + +typedef struct TMTPOINT TMTPOINT; +struct TMTPOINT{ + size_t r; + size_t c; +}; + +typedef struct TMTLINE TMTLINE; +struct TMTLINE{ + bool dirty; + TMTCHAR chars[]; +}; + +typedef struct TMTSCREEN TMTSCREEN; +struct TMTSCREEN{ + size_t nline; + size_t ncol; + + TMTLINE **lines; +}; + +/**** CALLBACK SUPPORT */ +typedef enum{ + TMT_MSG_MOVED, + TMT_MSG_UPDATE, + TMT_MSG_ANSWER, + TMT_MSG_BELL, + TMT_MSG_CURSOR +} tmt_msg_t; + +typedef void (*TMTCALLBACK)(tmt_msg_t m, struct TMT *v, const void *r, void *p); + +/**** PUBLIC FUNCTIONS */ +TMT *tmt_open(size_t nline, size_t ncol, TMTCALLBACK cb, void *p, + const wchar_t *acs); +void tmt_close(TMT *vt); +bool tmt_resize(TMT *vt, size_t nline, size_t ncol); +void tmt_write(TMT *vt, const char *s, size_t n); +const TMTSCREEN *tmt_screen(const TMT *vt); +const TMTPOINT *tmt_cursor(const TMT *vt); +void tmt_clean(TMT *vt); +void tmt_reset(TMT *vt); + +#endif diff --git a/confluent_vtbufferd/vtbufferd.c b/confluent_vtbufferd/vtbufferd.c new file mode 100644 index 00000000..9dee5cb1 --- /dev/null +++ b/confluent_vtbufferd/vtbufferd.c @@ -0,0 +1,143 @@ +#include +#include +#include +#include +#include +#include "tmt.h" +#define HASHSIZE 2053 +#define MAXNAMELEN 256 +#define MAXDATALEN 8192 +struct terment { + struct terment *next; + char *name; + TMT *vt; +}; + +#define SETNODE 1 +#define WRITE 2 +#define READBUFF 0 +static struct terment *buffers[HASHSIZE]; + +unsigned long hash(char *str) +/* djb2a */ +{ + unsigned long idx = 5381; + int c; + + while ((c = *str++)) + idx = ((idx << 5) + idx) + c; + return idx % HASHSIZE; +} + +TMT *get_termentbyname(char *name) { + struct terment *ret; + for (ret = buffers[hash(name)]; ret != NULL; ret = ret->next) + if (strcmp(name, ret->name) == 0) + return ret->vt; + return NULL; +} + +TMT *set_termentbyname(char *name) { + struct terment *ret; + int idx; + + idx = hash(name); + for (ret = buffers[idx]; ret != NULL; ret = ret->next) + if (strcmp(name, ret->name) == 0) + return ret->vt; + ret = (struct terment *)malloc(sizeof(*ret)); + ret->next = buffers[idx]; + ret->name = strdup(name); + ret->vt = tmt_open(31, 100, NULL, NULL, L"→←↑↓■◆▒°±▒┘┐┌└┼⎺───⎽├┤┴┬│≤≥π≠£•"); + buffers[idx] = ret; + return ret->vt; +} + +void dump_vt(TMT* outvt) { + const TMTSCREEN *out = tmt_screen(outvt); + const TMTPOINT *curs = tmt_cursor(outvt); + int line, idx, deferredlines, deferredspaces; + bool printedline, skipnl; + wprintf(L"\033[H\033[J"); + deferredlines = 0; + skipnl = true; + for (line = 0; line < out->nline; line++) { + deferredspaces = 0; + printedline = false; + for (idx = 0; idx < out->ncol; idx++) { + if (out->lines[line]->chars[idx].c == L' ') { + deferredspaces += 1; + } else { + if (!printedline) { + printedline = true; + if (skipnl) + skipnl = false; + else + wprintf(L"\r\n"); + } + while (deferredlines) { + wprintf(L"\r\n"); + deferredlines -= 1; + } + while (deferredspaces > 0) { + wprintf(L" "); + deferredspaces -= 1; + } + wprintf(L"%lc", out->lines[line]->chars[idx].c); + } + } + if (!printedline) + deferredlines += 1; + } + fflush(stdout); + wprintf(L"\x1b[%ld;%ldH", curs->r + 1, curs->c + 1); + fflush(stdout); + idx = write(1, "\x00", 1); + if (idx < 0) { + return; + } +} + +int main(int argc, char* argv[]) { + int cmd, length; + setlocale(LC_ALL, ""); + char cmdbuf[MAXDATALEN]; + char currnode[MAXNAMELEN]; + TMT *currvt = NULL; + TMT *outvt = NULL; + stdin = freopen(NULL, "rb", stdin); + if (stdin == NULL) { + exit(1); + } + while (1) { + length = fread(&cmd, 4, 1, stdin); + if (length < 0) + continue; + length = cmd & 536870911; + cmd = cmd >> 29; + if (cmd == SETNODE) { + currnode[length] = 0; + cmd = fread(currnode, 1, length, stdin); + if (cmd < 0) + continue; + currvt = set_termentbyname(currnode); + } else if (cmd == WRITE) { + if (currvt == NULL) + currvt = set_termentbyname(""); + cmdbuf[length] = 0; + cmd = fread(cmdbuf, 1, length, stdin); + if (cmd < 0) + continue; + tmt_write(currvt, cmdbuf, length); + } else if (cmd == READBUFF) { + cmdbuf[length] = 0; + cmd = fread(cmdbuf, 1, length, stdin); + if (cmd < 0) + continue; + outvt = get_termentbyname(cmdbuf); + if (outvt != NULL) { + dump_vt(outvt); + } + } + } +}