/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│ │vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi│ ╞══════════════════════════════════════════════════════════════════════════════╡ │ Copyright 2020 Justine Alexandra Roberts Tunney │ │ │ │ This program is free software; you can redistribute it and/or modify │ │ it under the terms of the GNU General Public License as published by │ │ the Free Software Foundation; version 2 of the License. │ │ │ │ This program is distributed in the hope that it will be useful, but │ │ WITHOUT ANY WARRANTY; without even the implied warranty of │ │ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU │ │ General Public License for more details. │ │ │ │ You should have received a copy of the GNU General Public License │ │ along with this program; if not, write to the Free Software │ │ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA │ │ 02110-1301 USA │ ╚─────────────────────────────────────────────────────────────────────────────*/ #include "dsp/scale/cdecimate2xuint8x8.h" #include "libc/bits/bits.h" #include "libc/bits/hilbert.h" #include "libc/bits/morton.h" #include "libc/bits/safemacros.internal.h" #include "libc/calls/calls.h" #include "libc/calls/ioctl.h" #include "libc/calls/struct/sigaction.h" #include "libc/calls/struct/siginfo.h" #include "libc/calls/struct/stat.h" #include "libc/calls/struct/termios.h" #include "libc/calls/struct/winsize.h" #include "libc/calls/ucontext.h" #include "libc/conv/conv.h" #include "libc/conv/itoa.h" #include "libc/dce.h" #include "libc/errno.h" #include "libc/log/log.h" #include "libc/macros.h" #include "libc/nexgen32e/bsf.h" #include "libc/runtime/runtime.h" #include "libc/sock/sock.h" #include "libc/stdio/stdio.h" #include "libc/str/str.h" #include "libc/str/tpenc.h" #include "libc/sysv/consts/ex.h" #include "libc/sysv/consts/exit.h" #include "libc/sysv/consts/map.h" #include "libc/sysv/consts/o.h" #include "libc/sysv/consts/poll.h" #include "libc/sysv/consts/prot.h" #include "libc/sysv/consts/sig.h" #include "libc/sysv/consts/termios.h" #include "libc/time/time.h" #include "libc/unicode/unicode.h" #include "third_party/getopt/getopt.h" #define USAGE \ " [-hznmHNW] [-p PID] [PATH]\n\ \n\ DESCRIPTION\n\ \n\ Memory Viewer\n\ \n\ FLAGS\n\ \n\ -h help\n\ -z zoom\n\ -m morton ordering\n\ -H hilbert ordering\n\ -N natural scrolling\n\ -W white terminal background\n\ -p PID shows process virtual memory\n\ -f INT frames per second [default 10]\n\ \n\ SHORTCUTS\n\ \n\ z or + zoom\n\ Z or - unzoom\n\ ctrl+wheel zoom point\n\ wheel scroll\n\ l linearize\n\ m mortonize\n\ h hilbertify\n\ n next mapping\n\ N next mapping ending\n\ p prev mapping\n\ P prev mapping ending\n\ k up\n\ j down\n\ b page up\n\ space page down\n\ g home\n\ G end\n\ q quit\n\ \n" #define CTRL(C) ((C) ^ 0100) #define MAXZOOM 14 #define COLOR 253 #define LINEAR 0 #define MORTON 1 #define HILBERT 2 #define INTERRUPTED 0x1 #define RESIZED 0x2 #define MOUSE_LEFT_DOWN 0 #define MOUSE_MIDDLE_DOWN 1 #define MOUSE_RIGHT_DOWN 2 #define MOUSE_LEFT_UP 4 #define MOUSE_MIDDLE_UP 5 #define MOUSE_RIGHT_UP 6 #define MOUSE_LEFT_DRAG 32 #define MOUSE_MIDDLE_DRAG 33 #define MOUSE_RIGHT_DRAG 34 #define MOUSE_WHEEL_UP 64 #define MOUSE_WHEEL_DOWN 65 #define MOUSE_CTRL_WHEEL_UP 80 #define MOUSE_CTRL_WHEEL_DOWN 81 struct Ranges { long i; struct Range { long a; long b; } p[512]; }; static const signed char kThePerfectKernel[8] = {-1, -3, 3, 17, 17, 3, -3, -1}; static bool white; static bool natural; static bool mousemode; static int fd; static int pid; static int out; static int fps; static int zoom; static int order; static int action; static long tyn; static long txn; static long size; static long offset; static long lowest; static long highest; static long canvassize; static long buffersize; static long displaysize; static char *buffer; static uint8_t *canvas; static struct stat st; static struct Ranges ranges; static struct termios oldterm; static char path[PATH_MAX]; static char mapspath[PATH_MAX]; static int Write(const char *s) { return write(out, s, strlen(s)); } static void HideCursor(void) { Write("\e[?25l"); } static void ShowCursor(void) { Write("\e[?25h"); } static void EnableMouse(void) { mousemode = true; Write("\e[?1000;1002;1015;1006h"); } static void DisableMouse(void) { mousemode = false; Write("\e[?1000;1002;1015;1006l"); } static void LeaveScreen(void) { Write("\e[H\e[J"); } static void GetTtySize(void) { struct winsize wsize; wsize.ws_row = tyn + 1; wsize.ws_col = txn; getttysize(out, &wsize); tyn = MAX(2, wsize.ws_row) - 1; txn = MAX(17, wsize.ws_col) - 16; tyn = rounddown2pow(tyn); txn = rounddown2pow(txn); tyn = MIN(tyn, txn); } static void EnableRaw(void) { struct termios term; memcpy(&term, &oldterm, sizeof(term)); term.c_cc[VMIN] = 1; term.c_cc[VTIME] = 1; term.c_iflag &= ~(INPCK | ISTRIP | PARMRK | INLCR | IGNCR | ICRNL | IXON); term.c_lflag &= ~(IEXTEN | ICANON | ECHO | ECHONL); term.c_cflag &= ~(CSIZE | PARENB); term.c_cflag |= CS8; term.c_iflag |= IUTF8; ioctl(out, TCSETS, &term); } static void OnExit(void) { LeaveScreen(); ShowCursor(); DisableMouse(); ioctl(out, TCSETS, &oldterm); } static void OnSigInt(int sig, struct siginfo *sa, struct ucontext *uc) { action |= INTERRUPTED; } static void OnSigWinch(int sig, struct siginfo *sa, struct ucontext *uc) { action |= RESIZED; } static void Setup(void) { tyn = 80; txn = 24; action = RESIZED; ioctl(out, TCGETS, &oldterm); HideCursor(); EnableRaw(); EnableMouse(); atexit(OnExit); sigaction(SIGINT, &(struct sigaction){.sa_sigaction = OnSigInt}, NULL); sigaction(SIGWINCH, &(struct sigaction){.sa_sigaction = OnSigWinch}, NULL); } static noreturn void FailPath(const char *s, int rc) { Write("error: "); Write(s); Write(": "); Write(path); Write("\n"); exit(rc); } static void SetExtent(long lo, long hi) { lowest = lo; highest = hi; offset = MIN(hi, MAX(lo, offset)); } static void Open(void) { int err; if ((fd = open(path, O_RDONLY)) == -1) { FailPath("open() failed", errno); } fstat(fd, &st); size = st.st_size; SetExtent(0, size); } static void *Allocate(size_t n) { return mmap(NULL, n, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); } static void SetupCanvas(void) { if (canvassize) { munmap(canvas, canvassize); munmap(buffer, buffersize); } displaysize = ROUNDUP(ROUNDUP((tyn * txn) << zoom, 16), 1ul << zoom); canvassize = ROUNDUP(displaysize, FRAMESIZE); buffersize = ROUNDUP(tyn * txn * 16 + PAGESIZE, FRAMESIZE); canvas = Allocate(canvassize); buffer = Allocate(buffersize); } static long IndexSquare(long y, long x) { switch (order) { case LINEAR: return y * txn + x; case MORTON: return morton(y, x); case HILBERT: return hilbert(txn, y, x); default: return 0; } } static long Index(long y, long x) { long i; if (order == LINEAR) { i = 0; } else { i = x / tyn; x = x % tyn; } return i * tyn * tyn + IndexSquare(y, x); } static void PreventBufferbloat(void) { long double now, rate; static long double last; now = nowl(); rate = 1. / fps; if (now - last < rate) { dsleep(rate - (now - last)); } last = now; } static bool HasPendingInput(void) { struct pollfd fds[1]; fds[0].fd = 0; fds[0].events = POLLIN; fds[0].revents = 0; poll(fds, ARRAYLEN(fds), 0); return fds[0].revents & (POLLIN | POLLERR); } static int GetCurrentRange(void) { int i; if (ranges.i) { for (i = 0; i < ranges.i; ++i) { if (offset < ranges.p[i].a) return MAX(0, i - 1); if (offset < ranges.p[i].b) return i; } return ranges.i - 1; } else { return -1; } } static void Move(long d) { d <<= zoom; offset = MIN(highest, MAX(lowest, (offset + d) >> zoom << zoom)); } static void SetZoom(long y, long x, int d) { long a, b, i; if ((0 <= y && y < tyn) && (0 <= x && x < txn)) { i = Index(y, x); a = zoom; b = MIN(MAXZOOM, MAX(0, a + d)); zoom = b; Move((i << a) - (i << b)); SetupCanvas(); } } static void OnZoom(long y, long x) { SetZoom(y, x, +1); } static void OnUnzoom(long y, long x) { SetZoom(y, x, -1); } static void OnUp(void) { Move(-(txn)); } static void OnDown(void) { Move(txn); } static void OnPageUp(void) { Move(-(txn * (tyn - 2))); } static void OnPageDown(void) { Move(txn * (tyn - 2)); } static void OnHome(void) { offset = lowest; } static void OnEnd(void) { offset = MAX(lowest, highest - txn * tyn); } static void OnLinear(void) { order = LINEAR; GetTtySize(); SetupCanvas(); } static void OnMorton(void) { order = MORTON; SetupCanvas(); } static void OnHilbert(void) { order = HILBERT; SetupCanvas(); } static void OnNext(void) { int i; if ((i = GetCurrentRange()) != -1) { if (i + 1 < ranges.i) { offset = ranges.p[i + 1].a; } } } static void OnPrev(void) { int i; if ((i = GetCurrentRange()) != -1) { if (i) { offset = ranges.p[i - 1].a; } } } static void OnNextEnd(void) { long i, n; if ((i = GetCurrentRange()) != -1) { n = (tyn * txn) << zoom; if (offset < ranges.p[i].b - n) { offset = ranges.p[i].b - n; } else if (i + 1 < ranges.i) { offset = MAX(ranges.p[i + 1].a, ranges.p[i + 1].b - n); } } } static void OnPrevEnd(void) { long i, n; if ((i = GetCurrentRange()) != -1) { n = (tyn * txn) << zoom; if (i) { offset = MAX(ranges.p[i - 1].a, ranges.p[i - 1].b - n); } } } static void OnMouse(char *p) { int e, x, y; e = strtol(p, &p, 10); if (*p == ';') ++p; x = min(txn, max(1, strtol(p, &p, 10))) - 1; if (*p == ';') ++p; y = min(tyn, max(1, strtol(p, &p, 10))) - 1; e |= (*p == 'm') << 2; switch (e) { case MOUSE_WHEEL_UP: if (natural) { OnDown(); OnDown(); OnDown(); } else { OnUp(); OnUp(); OnUp(); } break; case MOUSE_WHEEL_DOWN: if (natural) { OnUp(); OnUp(); OnUp(); } else { OnDown(); OnDown(); OnDown(); } break; case MOUSE_CTRL_WHEEL_UP: if (natural) { OnZoom(y, x); } else { OnUnzoom(y, x); } break; case MOUSE_CTRL_WHEEL_DOWN: if (natural) { OnUnzoom(y, x); } else { OnZoom(y, x); } break; default: break; } } static void ReadKeyboard(void) { char buf[32], *p = buf; memset(buf, 0, sizeof(buf)); if (readansi(0, buf, sizeof(buf)) == -1) { if (errno == EINTR) return; exit(errno); } switch (*p++) { case 'q': exit(0); case '+': case 'z': OnZoom(0, 0); break; case '-': case 'Z': OnUnzoom(0, 0); break; case 'b': OnPageUp(); break; case 'n': OnNext(); break; case 'p': OnPrev(); break; case 'N': OnNextEnd(); break; case 'P': OnPrevEnd(); break; case ' ': case CTRL('V'): OnPageDown(); break; case 'g': OnHome(); break; case 'G': OnEnd(); break; case 'k': case CTRL('P'): OnUp(); break; case 'j': case CTRL('N'): OnDown(); break; case 'l': OnLinear(); break; case 'm': if (order == MORTON) { OnLinear(); } else { OnMorton(); } break; case 'M': if (mousemode) { DisableMouse(); } else { EnableMouse(); } break; case 'h': case 'H': if (order == HILBERT) { OnLinear(); } else { OnHilbert(); } break; case '\e': switch (*p++) { case 'v': OnPageUp(); break; case '[': switch (*p++) { case '<': OnMouse(p); break; case 'A': OnUp(); break; case 'B': OnDown(); break; case 'F': OnEnd(); break; case 'H': OnHome(); break; case '1': switch (*p++) { case '~': OnHome(); break; default: break; } break; case '4': switch (*p++) { case '~': OnEnd(); break; default: break; } break; case '5': switch (*p++) { case '~': OnPageUp(); break; default: break; } break; case '6': switch (*p++) { case '~': OnPageDown(); break; default: break; } break; case '7': switch (*p++) { case '~': OnHome(); break; default: break; } break; case '8': switch (*p++) { case '~': OnEnd(); break; default: break; } break; default: break; } break; default: break; } break; default: break; } } static void LoadRanges(void) { char b[512]; struct Range range; int i, t, n, fd, err; if ((fd = open(mapspath, O_RDONLY)) == -1) { err = errno; Write("error: process died\n"); exit(err); } t = 0; range.a = 0; range.b = 0; ranges.i = 0; for (;;) { if ((n = read(fd, b, sizeof(b))) == -1) exit(1); if (!n) break; for (i = 0; i < n; ++i) { switch (t) { case 0: if (isxdigit(b[i])) { range.a <<= 4; range.a += hextoint(b[i]); } else if (b[i] == '-') { t = 1; } break; case 1: if (isxdigit(b[i])) { range.b <<= 4; range.b += hextoint(b[i]); } else if (b[i] == ' ') { t = 2; } break; case 2: if (b[i] == '\n') { if (ranges.i < ARRAYLEN(ranges.p)) { ranges.p[ranges.i++] = range; } range.a = 0; range.b = 0; t = 0; } break; default: unreachable; } } } close(fd); if (ranges.i) { SetExtent(ranges.p[0].a, ranges.p[ranges.i - 1].b); } else { SetExtent(0, 0); } } static int InvertXtermGreyscale(int x) { return -(x - 232) + 255; } static void Render(void) { char *p; int c, fg2, rc, fg; long i, y, x, w, n, got; p = buffer; p = stpcpy(p, "\e[H"); for (y = 0; y < tyn; ++y) { fg = -1; for (x = 0; x < txn; ++x) { c = canvas[Index(y, x)]; if (c < 32) { fg2 = 237 + c * ((COLOR - 237) / 32.); } else if (c >= 232) { fg2 = COLOR + (c - 232) * ((255 - COLOR) / (256. - 232)); } else { fg2 = COLOR; } if (fg2 != fg) { fg = fg2; if (white) { fg = InvertXtermGreyscale(fg); } p = stpcpy(p, "\e[38;5;"); p += int64toarray_radix10(fg, p); *p++ = 'm'; } w = tpenc(kCp437[c]); do { *p++ = w & 0xff; w >>= 8; } while (w); } p = stpcpy(p, "\e[0m "); p += uint64toarray_radix16(offset + ((y * txn) << zoom), p); p = stpcpy(p, "\e[K\r\n"); } p = stpcpy(p, "\e[7m\e[K"); n = strlen(path); if (n > txn - 3 - 1 - 7) { p = mempcpy(p, path, txn - 1 - 7 - 3); p = stpcpy(p, "..."); } else { p = stpcpy(p, path); for (i = n; i < txn - 1 - 7; ++i) { *p++ = ' '; } } p = stpcpy(p, " memzoom\e[0m "); if (!pid) { p += uint64toarray_radix10(MIN(offset / (long double)size * 100, 100), p); p = stpcpy(p, "%-"); p += uint64toarray_radix10( MIN((offset + ((tyn * txn) << zoom)) / (long double)size * 100, 100), p); p = stpcpy(p, "% "); } p += uint64toarray_radix10(1L << zoom, p); p = stpcpy(p, "x\e[J"); PreventBufferbloat(); for (i = 0, n = p - buffer; i < n; i += got) { got = 0; if ((rc = write(out, buffer + i, n - i)) == -1) { if (errno == EINTR) continue; exit(errno); } got = rc; } } static void Zoom(long have) { long i, n, r; n = canvassize; for (i = 0; i < zoom; ++i) { cDecimate2xUint8x8(n, canvas, kThePerfectKernel); n >>= 1; } if (n < tyn * txn) { memset(canvas + n, 0, canvassize - n); } if (have != -1) { n = have >> zoom; i = n / txn; r = n % txn; if (r) ++i; if (order == LINEAR) { for (; i < tyn; ++i) { canvas[txn * i] = '~'; } } } } static void FileZoom(void) { long have; have = MIN(displaysize, size - offset); have = pread(fd, canvas, have, offset); have = MAX(0, have); memset(canvas + have, 0, canvassize - have); Zoom(have); Render(); } static void RangesZoom(void) { long a, b, c, d, i; LoadRanges(); memset(canvas, 1, canvassize); a = offset; b = MIN(highest, offset + ((tyn * txn) << zoom)); for (i = 0; i < ranges.i; ++i) { if ((a >= ranges.p[i].a && a < ranges.p[i].b) || (b >= ranges.p[i].a && b < ranges.p[i].b) || (a < ranges.p[i].a && b >= ranges.p[i].b)) { c = MAX(a, ranges.p[i].a); d = MIN(b, ranges.p[i].b); pread(fd, canvas + (c - offset), d - c, c); } } Zoom(-1); Render(); } static void MemZoom(void) { do { if (action & RESIZED) { GetTtySize(); SetupCanvas(); action &= ~RESIZED; } if (HasPendingInput()) { ReadKeyboard(); continue; } if (pid) { RangesZoom(); } else { FileZoom(); } } while (!(action & INTERRUPTED)); } static noreturn void PrintUsage(int rc) { Write("SYNOPSIS\n\n "); Write(program_invocation_name); Write(USAGE); exit(rc); } static void GetOpts(int argc, char *argv[]) { int opt; char *p; fps = 10; while ((opt = getopt(argc, argv, "hzHNWf:p:")) != -1) { switch (opt) { case 'z': ++zoom; break; case 'm': order = MORTON; break; case 'H': order = HILBERT; break; case 'W': white = true; break; case 'N': natural = true; break; case 'f': fps = strtol(optarg, NULL, 0); fps = MAX(1, fps); break; case 'p': if (strcmp(optarg, "self") == 0) { pid = getpid(); } else { pid = strtol(optarg, NULL, 0); } break; case 'h': PrintUsage(EXIT_SUCCESS); default: PrintUsage(EX_USAGE); } } if (pid) { p = stpcpy(path, "/proc/"); p += int64toarray_radix10(pid, p); stpcpy(p, "/mem"); p = stpcpy(mapspath, "/proc/"); p += int64toarray_radix10(pid, p); stpcpy(p, "/maps"); } else { if (optind == argc) { PrintUsage(EX_USAGE); } if (!memccpy(path, argv[optind], '\0', sizeof(path))) { PrintUsage(EX_USAGE); } } } int main(int argc, char *argv[]) { if (!NoDebug()) showcrashreports(); out = 1; GetOpts(argc, argv); Open(); Setup(); MemZoom(); return 0; }