B4BY.588
Home
Terminal
Upload
information
Create File
Create Folder
About
Tools
:
/
disk1
/
worms
/
padel
/
2026may20
/
gsocket
/
tools
/
Filename :
console.c
back
Copy
/* * Process console-action (ctrl keys) and console-commands and dispatch * the action. * * Handle drawing of console and command line input. * * https://invisible-island.net/xterm/ctlseqs/ctlseqs.html * http://ascii-table.com/ansi-escape-sequences-vt-100.php * https://www.andreasen.org/letters/vt100.txt * https://www.linuxquestions.org/questions/programming-9/get-cursor-position-in-c-947833/ */ #include "common.h" #include <dirent.h> #include "pkt_mgr.h" #include "console.h" #include "console_display.h" #include "utils.h" #define ESCAPE(string) "\033" string // #define PTY_RESIZE_STR ESCAPE("7") ESCAPE("[r") ESCAPE("[9999;9999H") ESCAPE("[6n") // #define PTY_RESTORE ESCAPE("8") // #define PTY_SIZE_STR ESCAPE("[%d;%dR") #define UIntClr(dst,bits) dst = dst & (unsigned) ~(bits) #define GS_CONSOLE_PROMPT "#!ADM> " #define GS_CONSOLE_PROMPT_LEN (sizeof (GS_CONSOLE_PROMPT) - 1) // without \0 // 1 less than max input so that cursor on last pos looks better #define GS_CONSOLE_INPUT_LEN (gopt.winsize.ws_col - GS_CONSOLE_PROMPT_LEN - 1) #define GS_CON_SB_MAX_USERLEN 8 // StatusBar Max User Len static void console_reset(void); static void console_start(void); static void console_stop(void); static int console_command(struct _peer *p, const char *cmd); static uint8_t chr_last; static int tty_fd = -1; static int stdout_fd = -1; static void console_draw(int fd, int force); static int is_init_called; static GS_RL_CTX rl; static int is_console_welcome_msg; static int is_console_cursor_needs_reset; static const char *sb_color = "\x1B[44m\x1B[30m"; // Black on Blue #define GS_CONSOLE_BUF_SIZE (1024) #define GS_CONDIS_ROWS (GS_CONSOLE_ROWS - 2) enum _gs_ut_cursor_flags { GS_UT_CURSOR_ON = 0x01, GS_UT_CURSOR_OFF = 0x02 }; enum _gs_ut_cursor_flags ut_cursor; struct _console_info { char statusbar[512]; size_t sb_len; int is_sb_redraw_needed; int is_prompt_redraw_needed; float ping_ms; uint8_t n_users; int load; char user[14]; int sec_idle; // Bytes Per Second double last_usec; int64_t last_pos; double bps; double last_bps; float ft_last_perc; float ft_perc; // FileTransfer percent completion }; struct _console_info ci; GS_CONDIS gs_condis; // ConsoleDisplay static double get_usec(void) { struct timeval tv; gettimeofday(&tv, NULL); return (double)tv.tv_sec * 1000000 + tv.tv_usec; } static void console_init(int fd) { if (is_init_called) return; // Use YELLOW if TOR (socks) is being used if (gopt.gs_ctx.socks_ip != 0) sb_color = "\x1B[43m\x1B[30m"; // Black on Yellow DEBUGF_R("prompt size %zd\n", GS_CONSOLE_PROMPT_LEN); is_init_called = 1; // Set also if any of the calls below fail ci.last_usec = get_usec(); GS_RL_init(&rl, gopt.winsize.ws_col - GS_CONSOLE_PROMPT_LEN); // GS_RL_init(&rl, 10); GS_condis_init(&gs_condis, fd, GS_CONDIS_ROWS /* 3*/); stdout_fd = fd; // tty_fd = fd; // mad but works 99% if tty fails char *tty_name = ttyname(fd); if (tty_name == NULL) return; tty_fd = open(tty_name, O_RDWR | O_NOCTTY); if (tty_fd < 0) return; int rv; struct termios tio; rv = tcgetattr(tty_fd, &tio); if (rv != 0) return; UIntClr(tio.c_iflag, ICRNL); UIntClr(tio.c_lflag, (ICANON | ECHO)); tio.c_cflag |= CS8; tio.c_cc[VMIN] = 6; tio.c_cc[VTIME] = 1; rv = tcsetattr(tty_fd, TCSADRAIN, &tio); if (rv != 0) return; } static ssize_t tty_write(void *src, size_t len) { errno = ENOTTY; if (tty_fd < 0) return -1; errno = 0; return write(tty_fd, src, len); } static int is_cursor_in_console; // For the upper tier we do not know the coordinates. // Rely on Saved-cursor position instead. static void cursor_to_ut(void) { char buf[64]; char *end = buf + sizeof (buf); char *ptr = buf; // If Upper Tier disabled the cursor then do NOT show it. if (ut_cursor == GS_UT_CURSOR_OFF) SXPRINTF(ptr, end - ptr, "\x1B[?25l"); DEBUGF_C("cursor-restore (cursor_to_upper_tier)\n"); SXPRINTF(ptr, end - ptr, "\x1B""8"); // Restore cursor tty_write(buf, ptr - buf); is_cursor_in_console = 0; } // For the lower tier we know exactly our cordinates. static void cursor_to_lt(void) { char buf[64]; char *end = buf + sizeof (buf); char *ptr = buf; int row = gopt.winsize.ws_row; int col = 1 + GS_CONSOLE_PROMPT_LEN + MIN(rl.pos, rl.visible_len); // DEBUGF_W("Cursor to CONSOLE (Lower Tier) (%d:%df)\n", row, col); SXPRINTF(ptr, end - ptr, "\x1B[%d;%df", row, col); // ESC[?2004l = Reset bracketed paste mode if (is_console_cursor_needs_reset) { SXPRINTF(ptr, end - ptr, "\x1B[?2004l"); is_console_cursor_needs_reset = 0; } // If Upper Tier disabled the cursor then show it in console // DEBUGF_R("ut-cursor = %d\n", ut_cursor); if (ut_cursor == GS_UT_CURSOR_OFF) SXPRINTF(ptr, end - ptr, "\x1B[?25h"); tty_write(buf, ptr - buf); is_cursor_in_console = 1; } // Add to string and increase visual counter #define VSADDF(xptr, xend, xv, a...) do{ \ size_t n; \ n = snprintf(xptr, xend-xptr, a); \ xv += n; \ xptr += n; \ } while(0) static void mk_statusbar(void) { char *ptr = ci.statusbar; char *end = ptr + sizeof ci.statusbar; int row = gopt.winsize.ws_row - (GS_CONSOLE_ROWS - 1); size_t vc = 0; // visible characters // DEBUGF_C("mk_statusbar() called\n"); SXPRINTF(ptr, end - ptr, "\x1B[%d;1f", row); SXPRINTF(ptr, end - ptr, "%s", sb_color); memset(ptr, ':', end - ptr); float ms = ci.ping_ms; if (ms >= 1000) VSADDF(ptr, end, vc, "[%1.01fs ]", ms / 1000); else VSADDF(ptr, end, vc, "[%3dms]", (int)ms); if (ci.load >= 1000) VSADDF(ptr, end, vc, "[Load %02.02f][User(%u) ", (float)ci.load / 100, ci.n_users); else VSADDF(ptr, end, vc, "[Load % 4.02f][User(%u) ", (float)ci.load / 100, ci.n_users); // User name VSADDF(ptr, end, vc, "%*s ", GS_CON_SB_MAX_USERLEN, ci.user); // IDLE timer if (ci.sec_idle < 100) { SXPRINTF(ptr, end - ptr, "\x1b[31m"); VSADDF(ptr, end, vc, "%2d sec", ci.sec_idle); SXPRINTF(ptr, end - ptr, "\x1B[30m"); } else if (ci.sec_idle / 60 < 100) VSADDF(ptr, end, vc, "%2d min", ci.sec_idle / 60); else VSADDF(ptr, end, vc, "*idle*"); VSADDF(ptr, end, vc, "]"); // BYTES/sec char buf[GS_FT_SPEEDSTR_MAXSIZE]; GS_format_bps(buf, sizeof buf, (int64_t)ci.bps, "/s"); VSADDF(ptr, end, vc, "[%s]", buf); // Percent of FileTransfer completed [99.2%] or [ 0.4%] or [-----] if (ci.ft_perc <= 0) { VSADDF(ptr, end, vc, "[-----]"); } else { char perc[5]; snprintf(perc, sizeof perc, "%1.1f", ci.ft_perc); VSADDF(ptr, end, vc, "[%4s%%]", perc); } // Fill until end size_t v_left = gopt.winsize.ws_col - vc; ptr += v_left + 1; SXPRINTF(ptr, end - ptr, "\x1B[0m"); // Reset color ci.sb_len = ptr - ci.statusbar; ci.is_sb_redraw_needed = 1; } static void update_bps(struct _peer *p) { double now_usec; int64_t cur_pos; now_usec = get_usec(); cur_pos = p->gs->bytes_read + p->gs->bytes_written; ci.last_bps = ci.bps; if (now_usec == ci.last_usec) ci.bps = 0; else ci.bps = (cur_pos - ci.last_pos) * 1000000 / (now_usec - ci.last_usec); // Slowly adjust BPS to make it appear less jumpy ci.bps = ci.last_bps + (ci.bps - ci.last_bps) * 0.8; if (ci.bps < 50) ci.bps = 0; ci.last_usec = now_usec; ci.last_pos = cur_pos; if (ci.last_bps != ci.bps) ci.is_sb_redraw_needed += 1; // Percentage of File Transfer ci.ft_last_perc = ci.ft_perc; GS_FT *ft = &p->ft; GS_FT_stats *s = &ft->stats; if (s->xfer_amount_scheduled == 0) ci.ft_perc = 0; else { float f = ((float)(s->xfer_amount * 100)/ s->xfer_amount_scheduled); ci.ft_perc = MIN(f, 99.9); } if (ci.ft_last_perc != ci.ft_perc) ci.is_sb_redraw_needed += 1; } void CONSOLE_update_pinginfo(struct _peer *p, float ms, int load, char *user, int sec_idle, uint8_t n_users) { int fd = p->fd_out; ci.ping_ms = ms; ci.load = load; ci.n_users = n_users; if (strlen(user) > GS_CON_SB_MAX_USERLEN) memcpy(user + GS_CON_SB_MAX_USERLEN - 2, "..", 3); snprintf(ci.user, sizeof ci.user, "%s", user); ci.sec_idle = sec_idle; mk_statusbar(); console_draw(fd, 0); } /* * Called when the window size changed (sigwinch) */ void CONSOLE_resize(struct _peer *p) { char buf[128]; char *ptr = buf; char *end = buf + sizeof buf; int delta; DEBUGF_R("RESIZE to %d;%d\n", gopt.winsize.ws_col, gopt.winsize.ws_row); if (gopt.is_console) { delta = gopt.winsize.ws_row - gopt.winsize_prev.ws_row; if (delta > 0) { // Longer: // Assign scrolling area. Will reset cursor to 1;1 SXPRINTF(ptr, end - ptr, "\x1b[1;%dr", gopt.winsize.ws_row - GS_CONSOLE_ROWS); // Restore cursor to upper tier SXPRINTF(ptr, end - ptr, "\x1B""8"); // Clear screen SXPRINTF(ptr, end - ptr, "\x1B[J"); } if (delta < 0) { // Shorter: DEBUGF_R("Shorter. ScrollingArea to %d\n", gopt.winsize.ws_row - GS_CONSOLE_ROWS); if (is_cursor_in_console) { DEBUGF_R("cursor is IN console\n"); SXPRINTF(ptr, end - ptr, "\x1B""8""\x1B[J"); SXPRINTF(ptr, end - ptr, "\x1B[%dA", 0-delta); SXPRINTF(ptr, end - ptr, "\x1B""7"); SXPRINTF(ptr, end - ptr, "\x1b[1;%dr", gopt.winsize.ws_row - GS_CONSOLE_ROWS); SXPRINTF(ptr, end - ptr, "\x1B""8"); } else { DEBUGF_R("cursor is UPPER TIER\n"); SXPRINTF(ptr, end - ptr, "\x1B[%dS", 0-delta); SXPRINTF(ptr, end - ptr, "\x1b[1;%dr", gopt.winsize.ws_row - GS_CONSOLE_ROWS); SXPRINTF(ptr, end - ptr, "\x1B""8""\x1B[%dA", 0-delta); } } // do nothing if idendical (no change) tty_write(buf, ptr - buf); } GS_condis_pos(&gs_condis, (gopt.winsize.ws_row - GS_CONSOLE_ROWS) + 1 + 1, gopt.winsize.ws_col); GS_RL_resize(&rl, GS_CONSOLE_INPUT_LEN, gopt.winsize.ws_row /*last row*/, 1 + GS_CONSOLE_PROMPT_LEN); mk_statusbar(); console_draw(p->fd_out, 1); } void CONSOLE_update_bps(struct _peer *p) { update_bps(p); if (gopt.is_console == 0) return; // Only redraw if there was a change if (ci.is_sb_redraw_needed) { mk_statusbar(); console_draw(p->fd_out, 0); } } /* * status bar (sb) is drawn (Normally black on blue background). */ static void GS_sb_draw(int force) { if ((force == 0) && (ci.is_sb_redraw_needed == 0)) return; ci.is_sb_redraw_needed = 0; tty_write(ci.statusbar, ci.sb_len); } static void GS_prompt_draw(int force) { char buf[512]; char *ptr = buf;; char *end = buf + sizeof buf; if ((force == 0) && (ci.is_prompt_redraw_needed == 0)) return; ci.is_prompt_redraw_needed = 0; ptr = buf; SXPRINTF(ptr, end - ptr, "\x1B[%d;1f" GS_CONSOLE_PROMPT "%s", gopt.winsize.ws_row /*last*/, rl.vline); tty_write(buf, ptr - buf); } /* * Position active cursor to user input in console */ static void GS_prompt_cursor(void) { char buf[64]; char *ptr = buf; char *end = buf + sizeof buf; SXPRINTF(ptr, end - ptr, "\x1B[%d;%zuf", gopt.winsize.ws_row /*last*/, 1 + GS_CONSOLE_PROMPT_LEN + MIN(rl.pos, rl.visible_len)); tty_write(buf, ptr - buf); } void CONSOLE_draw(int fd) { console_draw(fd, 0); } static void console_draw(int fd, int force) { if (gopt.is_console == 0) return; int redraw_needed = 0; redraw_needed += ci.is_sb_redraw_needed; redraw_needed += gs_condis.is_redraw_needed; redraw_needed += ci.is_prompt_redraw_needed; // DEBUGF_W("CONSOLE DRAW (force=%d, redraw_needed=%d, cursor-in-console=%d)\n", force, redraw_needed, is_cursor_in_console); if ((force == 0) && (redraw_needed == 0)) { // DEBUGF("nothing to draw..\n"); return; } if (is_cursor_in_console == 0) { // DEBUGF_G("saving cursor (draw)\n"); tty_write("\x1B""7", 2); // Save position (upper tier) } // Status Bar (Normally black on blue) GS_sb_draw(force); // Log messages GS_condis_draw(&gs_condis, force); // Prompt GS_prompt_draw(force); // Restore cursor position if (is_cursor_in_console == 0) { tty_write("\x1B""8", 2); // Restore position (upper tier) // DEBUGF_G("C restored...(draw)\n"); } else { // if (redraw_needed) GS_prompt_cursor(); } } /* * Up-Arrow ^[OA or ^[[A * Return 0 if more data is needed. * Return 1 otherwise (set *esc if arrow received) */ static int ca_last; static int check_arrow(int *esc, uint8_t c) { *esc = 0; if ((c == 0x1b) && (ca_last == 0)) { ca_last = c; return 0; // more data needed } // Not inside ^[ sequence (0x1b) if (ca_last == 0) return 1; if (ca_last == 0x1b) { if ((c == 'O') || (c == '[')) { ca_last = '['; return 0; } ca_last = 0; return 0; // unknonw escape (?) } if (ca_last == '[') { *esc = 1; ca_last = 0; return 1; } return 1; } /* * Check for any Ctrl+E in user input. * * 1. any == submit=any. Send submit. Return -1 * 2. ^E + E == submit=^E . Send submit. Return -1 * 3. ^E == do not submit. Return 0 * 4. ^E + <known> == do not submit. Open console. Return 'c' * 5. ^E + ^E == submit=^E . Send ^E + ^E. Return -2 * == behavior non-screen like: * 6. ^E + <other> == submit=other. Send ^E + submit. Return -2 * ==> behavior screen like (see *#1* below) * 6. ^E + <other> == do not submit. Return 0 * * Return 0 : Caller not to process received character (more data required). * Return -1: Caller to process character in *submit * Return -2: not used. * Return >0: Escaped character. */ int CONSOLE_check_esc(uint8_t c, uint8_t *submit) { int esc; // DEBUGF_Y("key = 0x%02x\n", c); if (chr_last == GS_CONSOLE_ESC) { if (check_arrow(&esc, c) == 0) return 0; // More data required chr_last = c; switch (c) { case 'A': // UP if (esc == 0) break; if (gopt.is_console == 0) return 0; // Ignore if no console DEBUGF_G("^E-UP received. Calling cursor_to_ut()\n"); cursor_to_ut(); return 0; case 'B': // DOWN if (esc == 0) break; if (gopt.is_console == 0) return 0; // Ignore if no console // Arrow Down cursor_to_lt(); return 0; case GS_CONSOLE_ESC_CHR: case GS_CONSOLE_ESC_LCHR: DEBUGF_Y("esc-chr (last=0x%02x, this=0x%02x)\n", GS_CONSOLE_ESC, c); *submit = GS_CONSOLE_ESC; return -1; case GS_CONSOLE_ESC: // ^E + ^E DEBUGF_Y("esc\n"); *submit = GS_CONSOLE_ESC; chr_last = 0; // reset or ^E+^E+any wont work return -1; } return c; // screen-like (*#1*) // Not reached (non-screen behavior) // *submit = c; // return -2; } chr_last = c; if (c == GS_CONSOLE_ESC) return 0; *submit = c; return -1; } void CONSOLE_reset(void) { char buf[1024]; char *ptr = buf; char *end = buf + sizeof (buf); if (tty_fd < 0) return; DEBUGF_R("Resetting scolling area (rows %d)\n", gopt.winsize.ws_row); if (gopt.is_console) { ptr = buf; // Reset scrolling area. Will set cursor to 1;1. // SXPRINTF(ptr, end - ptr, "\x1B[r"); SXPRINTF(ptr, end - ptr, "\x1b[1;%dr", gopt.winsize.ws_row); /* Move cursor to last line */ SXPRINTF(ptr, end - ptr, "\x1B[9999;9999H"); /* Restore cursor */ if (write(stdout_fd, buf, ptr - buf) != ptr - buf) ERREXIT("write()\n"); } close(tty_fd); tty_fd = -1; } struct _pat { char *data; size_t len; int type; }; static struct _pat cls_pattern[] = { {"\x1B[0J", 4, 1}, // Clear screen from cursor down {"\x1B[J", 3, 1}, // Clear screen from cursor down {"\x1B[2J", 4, 1}, // Clear entire screen {"\x1B""c", 2, 4} // Reset terminal to initial state }; static struct _pat sb_pattern[] = { {"\x1B[?1049h", 8, 2}, // Switch Alternate Screen Buffer (clears screen) {"\x1B[?1049l", 8, 3} // Switch Normal Screen Buffer (clears screen) }; /* * Parse output and check for a any terminal escape sequence that clears * the screen. * * FIXME-PERFORMANCE: Could substitute [J and [2J and [0J with code * that goes to last line, then clears line '[K' and then scrools up * x line to clear the screen. That way the console would not need * to be re-drawn on every 'clear screen' by the app. * * Return 0 if not found. * cls_code = 1 => Clear screen * cls_code = 2 => Switched to screen buffer * cls_code = 3 => Switched to normal buffer * * amount => Amount of data save to process (remaining is part of an * unfinished ansi sequence). */ // Parse through the ansi sequence until it is finished. // Return length of ansi sequence or 0 if more data is required (ansi sequence hasnt finished yet) static size_t ansi_until_end(uint8_t *src, size_t src_sz, int *ignore) { uint8_t *src_end = src + src_sz; uint8_t *src_orig = src; // Must start with ^[ XASSERT(*src == '\x1b', "src not starting with 0x1B (0x02%c)\n", *src); src += 1; *ignore = 0; while (src < src_end) { if (*src == '\x1B') { // Huh? An ESC inside an ESC sequence? *ignore = 1; return src - src_orig; } if (src > src_orig + 16) { // ESC sequence is to long. We are not interested.... *ignore = 1; return src - src_orig; } // Check for 2 octet ESC sequence that does not end in A-Za-z if (src_orig + 1 == src) { switch (*src) { case '8': case '7': case '>': case '<': case '=': case '\\': src++; return src - src_orig; } } // Check if this is the end of an ansi sequence if ((*src >= 'a') && (*src <= 'z')) { src++; return src - src_orig; } if ((*src >= 'A') && (*src <= 'Z')) { src++; return src - src_orig; } src++; } return 0; // Not enough data // src - src_orig; } static size_t ansi_until_esc(uint8_t *src, size_t src_sz, int *in_esc) { uint8_t *src_end = src + src_sz; uint8_t *src_orig = src; while (src < src_end) { if (*src == '\x1B') { *in_esc = 1; // DEBUGF("at pos %zd=0x%02x\n", src - src_orig, *src); break; } src++; } return src - src_orig; } static int in_esc; // Parse 'src' for an ansi sequence that we might be interested in. // *tail_len contains a number of bytes if there is an incomplete ansi-sequence (and we // do not have enough data yet) // // Return: Length of data in dst. static void ansi_parse(uint8_t *src, size_t src_sz, GS_BUF *dst, size_t *tail_len, int *cls_code, int *sb_code) { uint8_t *src_end = src + src_sz; size_t len; int ignore; *tail_len = 0; while (src < src_end) { if (in_esc) { len = ansi_until_end(src, src_end - src, &ignore); // DEBUGF("esc len=%zd, ignore=%d, dst=%zd, left=%zd\n", len, ignore, GS_BUF_USED(dst), src_end - src); if (len == 0) { // Not enough data DEBUGF_R("Not Enough Data. TAIL %zd\n", src_end - src); DEBUGF("esc len=%zd, ignore=%d, dst=%zd, left=%zd\n", len, ignore, GS_BUF_USED(dst), src_end - src); HEXDUMP(src, src_end - src); *tail_len = src_end - src; return; //break; } in_esc = 0; #ifdef DEBUG // Output some ANSI but ignore some often re-occuring codes: while (1) { // if (len <= 4) // break; // Ignore short ones...like [1m if ((len == 8) && (src[7] == 'm')) break; // Ingore [39;49m to debug 'top' if ((len == 5) && (src[4] == 'm')) break; // Ingore [39m to debug 'mc' if ((len == 4) && (src[3] == 'm')) break; // Ingore [1m to debug DEBUGF_B("ANSI %.*s\n", (int)len -1, src+1); break; } #endif if (ignore) { GS_BUF_add_data(dst, src, len); src += len; continue; } int is_substitute = 0; // Check if the Upper Tier (ut) wants the cursor prompt ON or OFF // Check for this even if the console is closed so that when we open the console // that the right cursor can be displayed is_substitute = 0; while (len == 6) { if (memcmp(src + 1, "[?25l", 5) == 0) ut_cursor = GS_UT_CURSOR_OFF; // OFF else if (memcmp(src + 1, "[?25h", 5) == 0) ut_cursor = GS_UT_CURSOR_ON; // ON else break; // DEBUGF_R("ut_cursor=%d, in-console=%d\n", ut_cursor, is_cursor_in_console); // If cursor is in console then ignore all requests if (is_cursor_in_console) { is_substitute = 1; src += len; break; } break; } if (is_substitute) continue; // Check for Bracketed paste mode [?2004l if (len == 8) { if (memcmp(src + 1, "[?2004l", 7) == 0) is_console_cursor_needs_reset = 0; else if (memcmp(src + 1, "[?2004h", 7) == 0) is_console_cursor_needs_reset = 1; } // If console is not open then we do not have to check any other ansi symboles if (gopt.is_console == 0) { GS_BUF_add_data(dst, src, len); src += len; continue; } // Replace [2J (clear entire screen) with move to last line. Clear Line. Clear from cursor up if ((len == 4) && (memcmp(src + 1, "[2J", 3) == 0)) { // Move to last line. [K => Clear until end of line. [1J => Clear up]] GS_BUF_printf(dst, "\x1b[%d;1f\x1b[K\x1b[1J", gopt.winsize.ws_row - GS_CONSOLE_ROWS); src += len; continue; } // [J = Clear from cursor down // I dont have a solution how to do this more efficient beside re-drawing entire console :/ // or the need to track the cursor position or request the position from the terminal. // If the cursor position is known (as cordinated) then it's easy: Set up new scrolling area. // Use [<nnn>S (scroll up n lines) to scroll black. // Reset scrolling area to original. // if ((len == 3) && (memcmp(src + 1, "[J", 2) == 0)) // { // src += len; // continue; // } if ((len == 3) && memcmp(src + 1, "[r", 2) == 0) { DEBUGF_R("Scrolling area reset received.\n"); if (gopt.is_console) { GS_BUF_printf(dst, "\x1B[1;%dr", gopt.winsize.ws_row - GS_CONSOLE_ROWS); src += len; continue; } } // Check if this was a cursor-position request that moved the course // outside its boundary (and into our console, like debian's top does (!)) // '\x1b' + '[1;1h' is_substitute = 0; while (1) { if (len < 6) break; // DEBUGF_W("len %d\n", len); if ((src[len-1] != 'H') && (src[len-1] != 'h')) break; // search for ';' between src+2 and src+len uint8_t *ptr = src+2; for (ptr = src + 2; ptr < src+len; ptr++) { if (*ptr == ';') break; } if (*ptr != ';') break; int row = atoi((char *)src+2); int col = atoi((char *)ptr+1); // DEBUGF_W("pos %d:%d\n", row, col); if (row > gopt.winsize.ws_row - GS_CONSOLE_ROWS) { DEBUGF_R("CURSOR MOVE outside area DENIED. Changed to: %d;%d\n", gopt.winsize.ws_row - GS_CONSOLE_ROWS, col); GS_BUF_printf(dst, "\x1B[%d;%dH\r\n", gopt.winsize.ws_row - GS_CONSOLE_ROWS, col); src += len; is_substitute = 1; } break; } if (is_substitute) continue; // Check for any ANSI sequence that may have cleared the screen: int i; for (i = 0; i < sizeof cls_pattern / sizeof *cls_pattern; i++) { if (cls_pattern[i].len != len) continue; if (memcmp(cls_pattern[i].data, src, len) != 0) continue; DEBUGF_W("CLS found %d\n", cls_pattern[i].type); *cls_code = cls_pattern[i].type; } // Check for any ANSI sequence that changed Screen Buffer for (i = 0; i < sizeof sb_pattern / sizeof *sb_pattern; i++) { if (sb_pattern[i].len != len) continue; if (memcmp(sb_pattern[i].data, src, len) != 0) continue; // Handle if we receive [?1049h + [?1049l in one go then do nothing. if (*sb_code == 0) { *sb_code = sb_pattern[i].type; } else { if (*sb_code != sb_pattern[i].type) *sb_code = 0; else *sb_code = sb_pattern[i].type; } DEBUGF_W("SB change found (%d), sb_code set to %d\n", sb_pattern[i].type, *sb_code); } // We are not interested to substitute it. Let it pass through. GS_BUF_add_data(dst, src, len); src += len; } else { // DEBUGF_Y("#%zd not in esc\n", src - src_orig); len = ansi_until_esc(src, src_end - src, &in_esc); GS_BUF_add_data(dst, src, len); src += len; // *src points to ESC or is done. } } } GS_BUF g_dst; GS_BUF g_ansi; /* * Buffered write to ansi terminal. All output to terminal needs to be analyzed * and checked for 'clear screen' ansi code. If found then the console needs * to be re-drawn as well. * * Also need to find a good place to inject ansi sequence to move cursor * to upper tier (if in console). * * We do this by buffering the output and to only write complete ansi sequences * and never an incomplete sequences (as it would then not be possible to issue * another ESC sequence to move the cursor). * * - Find a good place to inject to move cursor from console to upper tier * - If half way inside an ansi sequence then buffer the remaining * - Also return if an ansi sequence was sent that clears the screen */ // Parse ANSI: // 1. Find any ansi sequence that clears the screen (so we know when to draw our console again) // 2. Substitute ESC-sequences with our own to stop console from getting fucked. // 3. If the ESC-sequence stops half way then write *dst and record // the remaining sequence (if we have that much space) static ssize_t ansi_write(int fd, void *src, size_t src_len, int *cls_code, int *sb_code) { // size_t amount = 0; size_t tail_len = 0; size_t src_len_orig = src_len; if (!GS_BUF_IS_INIT(&g_dst)) { GS_BUF_init(&g_dst, 1024); GS_BUF_init(&g_ansi, 1024); } if (GS_BUF_USED(&g_ansi) > 0) { GS_BUF_add_data(&g_ansi, src, src_len); src = GS_BUF_DATA(&g_ansi); src_len = GS_BUF_USED(&g_ansi); } // HEXDUMP(src, src_len); ansi_parse(src, src_len, &g_dst, &tail_len, cls_code, sb_code); if (GS_BUF_USED(&g_dst) > 0) { if (write(fd, GS_BUF_DATA(&g_dst), GS_BUF_USED(&g_dst)) != GS_BUF_USED(&g_dst)) { DEBUGF_R("Failed to write() all data...\n"); // SHOULD NOT HAPPEN return -1; } } GS_BUF_empty(&g_dst); GS_BUF_empty(&g_ansi); if (tail_len > 0) { // Use memmove() here because src might be pointing to same data but further along GS_BUF_memmove(&g_ansi, src + src_len - tail_len, tail_len); } // From the caller's perspective this function has processed all data // and this function will buffer (if needed) any data not yet passed // to 'write()'. Thus return 'len' here to satisfy caller that all supplied // data is or will be processed. return src_len_orig; } static int is_console_before_sb; // before Alternate Screen Buffer /* * Parse all output and check if the screen is cleared. If so then * redraw the console (if console is active) * * Situation to think about: * - console is visible * - top switches to frame buffer (console is visible) * - console is turned off * - top exits. * -> Still need to parse all output to check when we switch back to normal * buffer. The console was VISIBILE before we entered 'top' and now * need to be diabled after exiting 'top'. * Midnight commander: * - console is visible * - launch mc. mc does not use Screen Buffer (t should!) but instead remembers * the max screen size. * - while in mc, turn the console off. Then exit mc. * - mc will set the scroll area as when the console was visibile (but it aint * anymore). * * FIXME unsolved nested console problem: * - Both consoles fight to use ^[7 to save the cursor position: * Start outter gs-netcat. Open console. Move cursor to (outter) Upper Tier. * * Start another gs-netcat (inner from within outter). Open console. Sends [7 to save * (*1) cursor position from (inner) Upper Tier. Both consoles are not open (inner & outter). * * Outter updates its StatusBar (every second): It sends a [7 to save cursor position * from its Upper Tier. * That [7 request will overwrite the [7 that was sent under (*1) and will now store the position * of the cursor from inners console prompt. When the inner wants to move cursor its upper tier * it will send a [8 but that will move the cursor to its own prompt and not to its upper tier. * * - Workaround: Always leave cursor in up-most tier. Use a 'fake' cursor for the console * (like '_', [7m) and if cursor is supposed to be in the console then make it invisible * in upper tier (but move it back to upper tier [while invisilbe] as soon as console * update is completed). * The tricky parts are two: * 1. The outter does not know if the inner has the cursor in the console or its Upper Tier. * What should happen if ESC-e UP is pressed in outter tier? Cursor ON or OFF? * -> Could be solved by inner sending an 'in-band' custom ESC-sequence to outter that outter * intercepts (or if there is no outter but xterm, then ignored by xterm). * 2. The inner does not know when to disable its own fake-cursor (e.g. when outter presses ESC-e DOWN) * to move from its (outter) Upper Tier to (outter) console. * There is no signal send to the inner that its cursor should be disabled because the outter * moved it into its own console. */ ssize_t CONSOLE_write(int fd, void *data, size_t len) { int is_detected_clearscreen = 0; int is_sb_detected = 0; /* Move cursor to upper tier if cursor inside console */ if (is_cursor_in_console) { // DEBUGF_C("CURSOR-restore\n"); if (ut_cursor == GS_UT_CURSOR_OFF) tty_write("\x1B[?25l\x1B""8", 6+2); // Restore cursor to upper tier else tty_write("\x1B""8", 2); // Restore cursor to upper tier } ssize_t sz; sz = ansi_write(fd, data, len, &is_detected_clearscreen, &is_sb_detected); // The write() to upper tier may have set some funky paste modes and // screen-buffer modes. Track this (even if console is currently // closed - because it may have been closed while in a screen-buffer). if (is_cursor_in_console) { // DEBUGF_C("CURSOR-save\n"); tty_write("\x1B""7", 2); // Save new cursor position after writing to upper tier } // Now check if console needs to be re-drawn if (is_sb_detected == 2) { // Switch to Alternate Screen Buffer detected is_console_before_sb = gopt.is_console; DEBUGF_W("saving is_console=%d\n", gopt.is_console); } // DEBUGF_G("cls = %d iscon-before = %d, iscon-now %d\n", is_detected_clearscreen, is_console_before_sb, gopt.is_console); if (is_sb_detected == 3) { // Switched to Normal Screen Buffer detected DEBUGF_W("saved-is-console=%d, is_console=%d\n", is_console_before_sb, gopt.is_console); if (is_console_before_sb != gopt.is_console) { // Console has changed while operating on screen buffer if (gopt.is_console == 0) console_stop(); else console_reset(); } } if (is_detected_clearscreen == 4) { DEBUGF_R("RESET of terminal detected.\n"); if (gopt.is_console) console_reset(); } // Now we can safely return (after we tracked the ansi codes). if (gopt.is_console == 0) return sz; if ((is_detected_clearscreen) || (is_sb_detected)) console_draw(fd, 1 /*force*/); if (is_cursor_in_console) cursor_to_lt(); return sz; } /* * Offer data to console for readline. If cursor is in console then * we shall read those data in readline style. * * Return 1 if data was for console. * Return 0 otherwise */ int CONSOLE_readline(struct _peer *p, void *data, size_t len) { int fd = p->fd_out; uint8_t *src = (uint8_t *)data; uint8_t *s_end = src + len; uint8_t key; int rv; int is_got_line = 0; if (!(is_cursor_in_console)) return 0; for (; src < s_end; src++) { rv = GS_RL_add(&rl, *src, &key, gopt.winsize.ws_row, 1 + GS_CONSOLE_PROMPT_LEN); if (rl.esc_len > 0) { if (write(fd, rl.esc_data, rl.esc_len) != rl.esc_len) ERREXIT("write()\n"); } if (rv < 0) { // HERE: Special character (like UP/DOWN or ENTER) if (key == '\n') { is_got_line = 1; break; } else if (key == 'A') { DEBUGF_Y("UP\n"); GS_condis_up(&gs_condis); CONSOLE_draw(gs_condis.fd); } else if (key == 'B') { GS_condis_down(&gs_condis); CONSOLE_draw(gs_condis.fd); } // Unhandled control character (ignore for input) continue; } } if (is_got_line) { console_command(p, rl.line); GS_RL_reset(&rl); } // DEBUGF("final line: '%s'\n", rl.line); return 1; } /* * Set up terminal to display console (e.g. scroll upper tier up * and save cursor location of upper tier). * Called when console starts or when change in screenbuffer is detected. */ static void console_reset(void) { char buf[GS_CONSOLE_BUF_SIZE]; char *end = buf + sizeof (buf); char *ptr = buf; int row; row = gopt.winsize.ws_row - GS_CONSOLE_ROWS; int i; // Scroll up i lines for (i = 0; i < GS_CONSOLE_ROWS; i++) SXPRINTF(ptr, end - ptr, "\x1B""D"); // Move cursor up. Then save cursor pos. SXPRINTF(ptr, end - ptr, "\x1B[%dA\x1B""7", GS_CONSOLE_ROWS); // Set scrolling area. Will set cursor to 1;1. DEBUGF("Setting Scrolling area to %d\n", row); SXPRINTF(ptr, end - ptr, "\x1b[1;%dr", row); // Restore cursor to saved location SXPRINTF(ptr, end - ptr, "\x1B""8"); tty_write(buf, ptr - buf); } static void console_start(void) { console_reset(); gopt.is_console = 1; cursor_to_lt(); // Start with cursor in console } /* * configure terminal back to normal (no console) */ static void console_stop(void) { char buf[GS_CONSOLE_BUF_SIZE]; char *end = buf + sizeof (buf); char *ptr = buf; // Clear console SXPRINTF(ptr, end - ptr, "\x1B[%d;1f", gopt.winsize.ws_row - GS_CONSOLE_ROWS); SXPRINTF(ptr, end - ptr, "\x1B[J"); // Reset scroll size SXPRINTF(ptr, end - ptr, "\x1B[r"); // Upper Tier wants cursor OFF if (ut_cursor == GS_UT_CURSOR_OFF) SXPRINTF(ptr, end - ptr, "\x1B[?25l"); // Restore cursor to upper tier (shell) SXPRINTF(ptr, end - ptr, "\x1B""8"); tty_write(buf, ptr - buf); is_cursor_in_console = 0; gopt.is_console = 0; } /* * Equivalent to ssh's ~. quick exit. */ static int hard_quit(void) { CONSOLE_reset(); stty_reset(); exit(0); // hard exit. } /* * Process single action keys (e.g. CTRL+E + <action>) */ int CONSOLE_action(struct _peer *p, uint8_t key) { console_init(p->fd_out); DEBUGF("\nConsole Key Action 0x%02x\n", key); if (key == 'q') hard_quit(); #ifdef DEBUG if (key == 'l') { DEBUGF_B("redraw\n"); mk_statusbar(); console_draw(p->fd_out, 1); } #endif if (key == 'c') { gopt.is_win_resized = 1; // Trigger: Send new window size to peer gopt.is_want_ids_on = 1; GS_SELECT_FD_SET_W(p->gs); if (gopt.is_console == 1) { // Close console and restore cursor console_stop(); return 0; } console_start(); GS_condis_pos(&gs_condis, (gopt.winsize.ws_row - GS_CONSOLE_ROWS) + 1 + 1, gopt.winsize.ws_col); if (is_console_welcome_msg == 0) { GS_condis_add(&gs_condis, 0, "Press Ctrl-e + c to close this console or Ctrl-e + q to quit."); GS_condis_add(&gs_condis, 0, "Press Ctrl-e + UP to leave the console."); GS_condis_add(&gs_condis, 0, "Press Ctrl-e + DOWN to enter the console."); GS_condis_add(&gs_condis, 0, "Use UP/DOWN to scroll through the console's log"); GS_condis_add(&gs_condis, 0, "Type 'help' for a list of commands."); is_console_welcome_msg = 1; } // Draw console needed? Resizing remote will trigger a CLEAR (=> re-draw) mk_statusbar(); console_draw(p->fd_out, 1); } return 0; } static void cmd_help(int fd) { GS_condis_add(&gs_condis, 0, "quit - Quit | Ctrl-e q : quit | Ctrl-e c : toggle console"); GS_condis_add(&gs_condis, 0, "ping - RTT to peer | Ctrl-e UP: Go Up | Ctrl-e DN: Go Down"); GS_condis_add(&gs_condis, 0, "put <file> - Upload file - Example: put /usr/./share/ma*"); GS_condis_add(&gs_condis, 0, "get <file> - Download file - Example: get ~/*.[ch]"); GS_condis_add(&gs_condis, 0, "Other commands: lls, lcd, lmkdir, lpwd, pwd"); GS_condis_draw(&gs_condis, 1); } // Use wordexp(3) to resolve path name with ~/ and variable substitution static int path_resolve(const char *pattern, char *dst, size_t len) { #ifdef HAVE_WORDEXP_H wordexp_t p; int ret; if (len <= 0) return -1; dst[0] = '\0'; // On failure return 'pattern' as path snprintf(dst, len, "%.*s", MAX(0, (int)len -1) , pattern); signal(SIGCHLD, SIG_DFL); ret = wordexp(pattern, &p, WRDE_NOCMD); signal(SIGCHLD, SIG_IGN); if (ret != 0) { DEBUGF_R("wordexp(%s) error: %d\n", pattern, ret); return -1; } if (p.we_wordc <= 0) { wordfree(&p); return -1; } snprintf(dst, len, "%s", p.we_wordv[0]); wordfree(&p); return 0; #else return -1; #endif } static const char * strip_space(const char *str) { while (*str == ' ') str++; return str; } // Output single file information static void cmd_lls_file(const char *name) { struct stat sr; if (stat(name, &sr) != 0) { // ERROR GS_condis_printf(&gs_condis, GS_PKT_APP_LOG_TYPE_DEFAULT, "%s: %s", strerror(errno), name); return; } #ifdef __APPLE__ struct timespec ts = sr.st_mtimespec; #else struct timespec ts = sr.st_mtim; #endif struct tm tm; localtime_r(&ts.tv_sec, &tm); // MS-DOS style output (oldskewl) char tmstr[32]; strftime(tmstr, sizeof tmstr, "%Y-%m-%d %H:%M", &tm); const char *typestr = "<\?\?\?>"; if (S_ISDIR(sr.st_mode)) typestr = "<DIR>"; else if (S_ISLNK(sr.st_mode)) typestr = "<LNK>"; else if (S_ISFIFO(sr.st_mode)) typestr = "<FIF>"; else if (S_ISBLK(sr.st_mode)) typestr = "<BLK>"; else if (S_ISCHR(sr.st_mode)) typestr = "<DEV>"; else if (S_ISREG(sr.st_mode)) typestr = ""; GS_condis_printf(&gs_condis, GS_PKT_APP_LOG_TYPE_DEFAULT, "%16s %5.5s %' 16"PRId64" %s", tmstr, typestr, (int64_t)sr.st_size, name); } // List local files. static void cmd_lls_single(const char *exp) { #ifdef HAVE_WORDEXP_H wordexp_t p; char **w; DIR *d = NULL; char buf[GS_PATH_MAX]; int ret; signal(SIGCHLD, SIG_DFL); ret = wordexp(exp, &p, 0); signal(SIGCHLD, SIG_IGN); if (ret != 0) return; // error (0 found) setlocale(LC_NUMERIC, ""); // for printf("'%d" thausand separator w = p.we_wordv; // If there is only ONE result and that result is a DIRECTORY then output the content // of that directory instead. (e.g. 'ls .' or 'ls /tmp') struct stat sr; if ((p.we_wordc == 1) && (stat(w[0], &sr) == 0) && S_ISDIR(sr.st_mode)) { // Opendir etc.. d = opendir(w[0]); if (d == NULL) { // ERROR GS_condis_printf(&gs_condis, GS_PKT_APP_LOG_TYPE_DEFAULT, "%s: %s", strerror(errno), w[0]); goto err; } struct dirent *entry; for (entry = readdir(d); entry != NULL; entry = readdir(d)) { if (memcmp(w[0], ".\0", 2) == 0) snprintf(buf, sizeof buf, "%s", entry->d_name); else snprintf(buf, sizeof buf, "%s/%s", w[0], entry->d_name); cmd_lls_file(buf); } } else { int i; for (i = 0; i < p.we_wordc; i++) cmd_lls_file(w[i]); } err: if (d != NULL) closedir(d); wordfree(&p); #else return; #endif } static void cmd_lls(const char *str) { char *orig = strdup(str); char *next; char *name = orig; while (name != NULL) { next = strchr(name, ' '); if (next != NULL) { *next = '\0'; next += 1; } cmd_lls_single(name); name = next; } XFREE(orig); } static int console_command(struct _peer *p, const char *cmd) { int fd = p->fd_out; char buf[GS_CONSOLE_BUF_SIZE]; char path[GS_PATH_MAX + 1]; char *end = buf + sizeof (buf); char *ptr; const char *arg; if (strlen(cmd) <= 0) return 0; if (memcmp(cmd, "help", 4) == 0) { cmd_help(fd); } else if (memcmp(cmd, "ping", 4) == 0) { cmd_ping(p); } else if (memcmp(cmd, "quit", 4) == 0) { hard_quit(); } else if (memcmp(cmd, "pwd", 3) == 0) { cmd_pwd(p); } else if (memcmp(cmd, "clear", 5) == 0) { GS_condis_clear(&gs_condis); GS_condis_draw(&gs_condis, 1); } else if (memcmp(cmd, "put ", 4) == 0) { GS_FT_put(&p->ft, cmd+4); GS_SELECT_FD_SET_W(p->gs); } else if (memcmp(cmd, "get ", 4) == 0) { GS_FT_get(&p->ft, cmd+4); GS_SELECT_FD_SET_W(p->gs); } else if (memcmp(cmd, "xaitax", 6) == 0) { GS_condis_add(&gs_condis, GS_PKT_APP_LOG_TYPE_DEFAULT, "Thanks xaitax for testing!"); GS_condis_draw(&gs_condis, 1); } else if (strncmp(cmd, "lpwd", 4) == 0) { char *cwd = getcwdx(); GS_condis_add(&gs_condis, GS_PKT_APP_LOG_TYPE_DEFAULT, cwd); XFREE(cwd); GS_condis_draw(&gs_condis, 1); } else if (strncmp(cmd, "lcd ", 4) == 0) { arg = strip_space(cmd + 4); path_resolve(arg, path, sizeof path); if (chdir(path) != 0) GS_condis_printf(&gs_condis, GS_PKT_APP_LOG_TYPE_DEFAULT, "%s: %.512s", strerror(errno), path); else { char *cwd = getcwdx(); GS_condis_printf(&gs_condis, GS_PKT_APP_LOG_TYPE_DEFAULT, "%s", cwd); XFREE(cwd); } GS_condis_draw(&gs_condis, 1); } else if (strncmp(cmd, "lmkdir ", 7) == 0) { arg = strip_space(cmd + 7); if (mkdir(arg, 0777) != 0) { GS_condis_printf(&gs_condis, GS_PKT_APP_LOG_TYPE_DEFAULT, "%s: %.512s", strerror(errno), arg); GS_condis_draw(&gs_condis, 1); } } else if (strncmp(cmd, "lls", 3) == 0) { arg = strip_space(cmd + 3); if (*arg == 0) arg = "."; // 'lls' should be 'lls .' (current directory) cmd_lls(arg); GS_condis_draw(&gs_condis, 1); } else { GS_condis_printf(&gs_condis, 0, "Command not known: '%s'", cmd); GS_condis_draw(&gs_condis, 1); } ptr = buf; SXPRINTF(ptr, end - ptr, "\x1B[%d;%zuf\x1B[K", gopt.winsize.ws_row, 1 + GS_CONSOLE_PROMPT_LEN); tty_write(buf, ptr - buf); return 0; }