1 /* 2 * udbg - ucode debugger client 3 * 4 * Copyright (C) 2026 Jo-Philipp Wich <jo@mein.io> 5 * 6 * Permission to use, copy, modify, and/or distribute this software for any 7 * purpose with or without fee is hereby granted, provided that the above 8 * copyright notice and this permission notice appear in all copies. 9 * 10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 * WITH REGARD TO THIS SOFTWARE INCLUDING ANY IMPLIED WARRANTIES OF 12 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 * 18 * --- 19 * 20 * Interactive client for ucode's line-based debug protocol (one uppercase 21 * VERB, optionally followed by a space and a JSON object, per '\n'-terminated 22 * line - see lib/debug_proto.h). This client owns all user-facing rendering: 23 * the server-side debug core never emits ANSI or formatted columns, only 24 * structured data (plus, where a rendering-rich port needed more than the 25 * original data model had - e.g. DISASSEMBLE's raw instruction bytes - a 26 * small additive extension of that same structured data, never markup). 27 * 28 * Three ways to obtain a connection: 29 * udbg <pid> - SIGUSR1-attach to a running `-X` process (gdb -p style) 30 * udbg <path> - connect to an explicit debug.listen(path) socket 31 * udbg --fd N - use an already-connected, inherited fd N (used 32 * internally by the local `-x` CLI, which forks this 33 * binary with one end of a socketpair on fd 3) 34 */ 35 36 #include <stdio.h> 37 #include <stdlib.h> 38 #include <string.h> 39 #include <unistd.h> 40 #include <errno.h> 41 #include <fcntl.h> 42 #include <signal.h> 43 #include <sys/socket.h> 44 #include <sys/un.h> 45 #include <sys/stat.h> 46 #include <sys/select.h> 47 #include <sys/ioctl.h> 48 #include <termios.h> 49 #include <ctype.h> 50 #include <inttypes.h> 51 #include <stdbool.h> 52 53 #include <json-c/json.h> 54 55 #include "debug_highlight.h" 56 #include "debug_lineedit.h" 57 58 /* -- ANSI colors ----------------------------------------------------------- */ 59 60 #define C_RESET "\033[0m" 61 #define C_DIM "\033[2m" 62 #define C_BOLD "\033[1m" 63 #define C_RED "\033[31m" 64 #define C_GREEN "\033[32m" 65 #define C_YELLOW "\033[33m" 66 #define C_BLUE "\033[34m" 67 #define C_MAGENTA "\033[35m" 68 #define C_CYAN "\033[36m" 69 #define C_EVENT "\033[2;3m" /* faint + italic, for async server events */ 70 71 #define MAX_LINE 65536 72 #define DEFAULT_SOCKET_DIR "/tmp" 73 #define MAX_WAIT_TIME 30 74 75 /* -- wire framing ---------------------------------------------------------- 76 * 77 * Mirrors lib/debug_proto.c's framing without depending on it: this client 78 * has no ucode VM of its own to hand `ucv_*` helpers, so it talks the wire 79 * format directly in terms of json-c objects instead. */ 80 81 static void 82 proto_write(int fd, const char *verb, struct json_object *payload) 83 { 84 const char *json; 85 char *line; 86 size_t len; 87 ssize_t n; 88 const char *p; 89 90 if (payload) { 91 json = json_object_to_json_string_ext(payload, JSON_C_TO_STRING_PLAIN); 92 len = strlen(verb) + 1 + strlen(json) + 1; 93 line = malloc(len + 1); 94 snprintf(line, len + 1, "%s %s\n", verb, json); 95 } 96 else { 97 len = strlen(verb) + 1; 98 line = malloc(len + 1); 99 snprintf(line, len + 1, "%s\n", verb); 100 } 101 102 p = line; 103 104 while (len > 0) { 105 n = write(fd, p, len); 106 107 if (n < 0) { 108 if (errno == EINTR) 109 continue; 110 111 break; 112 } 113 114 p += n; 115 len -= (size_t)n; 116 } 117 118 free(line); 119 } 120 121 /* Growable line-buffered reader, one instance per connection. */ 122 typedef struct { 123 char *data; 124 size_t len, cap; 125 } linebuf_t; 126 127 static bool 128 linebuf_append(linebuf_t *lb, const char *data, size_t n) 129 { 130 if (lb->len + n > lb->cap) { 131 size_t newcap = lb->cap ? lb->cap : 4096; 132 133 while (newcap < lb->len + n) 134 newcap *= 2; 135 136 char *p = realloc(lb->data, newcap); 137 138 if (!p) 139 return false; 140 141 lb->data = p; 142 lb->cap = newcap; 143 } 144 145 memcpy(lb->data + lb->len, data, n); 146 lb->len += n; 147 148 return true; 149 } 150 151 /* Extract one already-buffered "VERB [json]" line, if any, without touching 152 * the fd. Returns false if no full line is buffered yet. */ 153 static bool 154 linebuf_pop(linebuf_t *lb, char **verb_out, struct json_object **payload_out) 155 { 156 char *nl = memchr(lb->data, '\n', lb->len); 157 size_t linelen, verblen; 158 char *line, *sp; 159 160 if (!nl) 161 return false; 162 163 linelen = (size_t)(nl - lb->data); 164 line = malloc(linelen + 1); 165 memcpy(line, lb->data, linelen); 166 line[linelen] = '\0'; 167 168 memmove(lb->data, lb->data + linelen + 1, lb->len - linelen - 1); 169 lb->len -= linelen + 1; 170 171 if (linelen > 0 && line[linelen - 1] == '\r') 172 line[--linelen] = '\0'; 173 174 sp = memchr(line, ' ', linelen); 175 verblen = sp ? (size_t)(sp - line) : linelen; 176 177 *verb_out = malloc(verblen + 1); 178 memcpy(*verb_out, line, verblen); 179 (*verb_out)[verblen] = '\0'; 180 181 *payload_out = NULL; 182 183 if (sp && *(sp + 1)) 184 *payload_out = json_tokener_parse(sp + 1); 185 186 free(line); 187 188 return true; 189 } 190 191 /* Block until a full message is available on `fd`/`lb` and pop it - used for 192 * the synchronous SOURCE request/response round-trip triggered from within 193 * rendering. Only safe to call while the session is paused and no other 194 * request is outstanding (true for every call site below): the server only 195 * ever answers strictly in request order while paused, so the first message 196 * to arrive is the one we asked for, barring the rare case of an async 197 * EVENT interleaving, which is not handled specially here. */ 198 static const char * 199 jstr(struct json_object *obj, const char *key, const char *dflt) 200 { 201 struct json_object *v; 202 203 if (obj && json_object_object_get_ex(obj, key, &v) && json_object_is_type(v, json_type_string)) 204 return json_object_get_string(v); 205 206 return dflt; 207 } 208 209 static int64_t 210 jint(struct json_object *obj, const char *key, int64_t dflt) 211 { 212 struct json_object *v; 213 214 if (obj && json_object_object_get_ex(obj, key, &v)) 215 return json_object_get_int64(v); 216 217 return dflt; 218 } 219 220 /* Every "col"/"from_col"/"to_col" field the protocol sends is 1-based (see 221 * uc_source_get_line() in source.c), meant for human-readable "line:col" 222 * display - debug_highlight's span/ip columns are 0-based byte indices 223 * into the line string, so any such field needs this before being used as 224 * one. */ 225 static size_t 226 col0(int64_t col) 227 { 228 return (col > 0) ? (size_t)(col - 1) : 0; 229 } 230 231 /* -- source cache & syntax highlighting ----------------------------------- */ 232 233 typedef struct source_cache_entry { 234 char *file; 235 char **lines; 236 size_t nlines; 237 struct source_cache_entry *next; 238 } source_cache_entry_t; 239 240 static source_cache_entry_t *source_cache = NULL; 241 242 static char ** 243 split_lines(const char *text, size_t *nlines_out) 244 { 245 size_t count = 1, i; 246 char **lines; 247 const char *p, *start; 248 249 for (p = text; *p; p++) 250 if (*p == '\n') 251 count++; 252 253 lines = calloc(count, sizeof(char *)); 254 i = 0; 255 start = text; 256 257 for (p = text; ; p++) { 258 if (*p == '\n' || *p == '\0') { 259 size_t len = (size_t)(p - start); 260 261 if (len > 0 && start[len - 1] == '\r') 262 len--; 263 264 lines[i] = malloc(len + 1); 265 memcpy(lines[i], start, len); 266 lines[i][len] = '\0'; 267 i++; 268 269 if (*p == '\0') 270 break; 271 272 start = p + 1; 273 } 274 } 275 276 *nlines_out = i; 277 278 return lines; 279 } 280 281 static char ** 282 find_cached_source(const char *file, size_t *nlines_out) 283 { 284 source_cache_entry_t *e; 285 286 for (e = source_cache; e; e = e->next) { 287 if (!strcmp(e->file, file)) { 288 *nlines_out = e->nlines; 289 290 return e->lines; 291 } 292 } 293 294 return NULL; 295 } 296 297 /* Split and cache already-known source `text` for `file` (e.g. from a 298 * SOURCE response the caller already has in hand), so a later 299 * render_source_lines() call for the same file doesn't re-request it. */ 300 static char ** 301 cache_source_text(const char *file, const char *text, size_t *nlines_out) 302 { 303 source_cache_entry_t *e; 304 char **cached = find_cached_source(file, nlines_out); 305 306 if (cached) 307 return cached; 308 309 e = malloc(sizeof(source_cache_entry_t)); 310 e->file = strdup(file); 311 e->lines = split_lines(text, &e->nlines); 312 e->next = source_cache; 313 source_cache = e; 314 315 *nlines_out = e->nlines; 316 317 return e->lines; 318 } 319 320 /* Local source root override (-s/--srcdir), used when the path the server 321 * reports doesn't exist as-is on this machine - see try_load_local_file(). */ 322 static const char *opt_srcdir = NULL; 323 324 static char * 325 read_whole_file(FILE *fp) 326 { 327 char buf[65536]; 328 size_t n, cap = 0, len = 0; 329 char *text = NULL; 330 331 while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) { 332 if (len + n + 1 > cap) { 333 cap = cap ? cap * 2 : 65536; 334 335 while (cap < len + n + 1) 336 cap *= 2; 337 338 text = realloc(text, cap); 339 } 340 341 memcpy(text + len, buf, n); 342 len += n; 343 } 344 345 if (!text) 346 text = malloc(1); 347 348 text[len] = '\0'; 349 350 return text; 351 } 352 353 /* Debugging usually either runs fully locally (the client and the debugged 354 * script share the same filesystem - the common `-x`/`udbg <pid>`-on-the- 355 * same-box case) or from a development checkout against a remote target 356 * (the *client* has the better/only real source access, not the server) - 357 * in both cases, the client reading the file itself is at least as likely 358 * to succeed as asking the server for it, and doesn't need a round trip. 359 * Only once this fails do callers fall back to requesting SOURCE from the 360 * server (e.g. the target is a remote embedded device with no shared 361 * filesystem, or running precompiled bytecode with only embedded source). 362 * 363 * Tries the path exactly as the server reported it first (already correct 364 * for the local case, and for absolute paths that happen to also exist on 365 * this machine), then, if `-s/--srcdir DIR` was given, DIR joined with 366 * just the reported path's basename - a simple heuristic for "the server's 367 * path is from a different checkout/build root than this one". */ 368 static char ** 369 try_load_local_file(const char *file, size_t *nlines_out) 370 { 371 FILE *fp = fopen(file, "rb"); 372 char *joined = NULL; 373 374 if (!fp && opt_srcdir) { 375 const char *base = strrchr(file, '/'); 376 377 base = base ? base + 1 : file; 378 joined = malloc(strlen(opt_srcdir) + 1 + strlen(base) + 1); 379 sprintf(joined, "%s/%s", opt_srcdir, base); 380 fp = fopen(joined, "rb"); 381 } 382 383 free(joined); 384 385 if (!fp) 386 return NULL; 387 388 { 389 char *text = read_whole_file(fp); 390 char **lines = cache_source_text(file, text, nlines_out); 391 392 fclose(fp); 393 free(text); 394 395 return lines; 396 } 397 } 398 399 /* Rendering a source range/context needs the actual text, which only ever 400 * arrives asynchronously as a SOURCE response processed by the normal main 401 * loop - never via a nested blocking round-trip from inside another 402 * response's rendering, which would re-enter the single shared connection 403 * state from two places at once. So when the file isn't cached yet, a 404 * render call fires off a SOURCE request and remembers what it wanted to 405 * show as `pending_source`; the main loop's SOURCE handler finishes the 406 * render once the response actually arrives. */ 407 typedef struct { 408 bool active; 409 char *file; 410 int64_t from, to; 411 debug_highlight_span_t hl; 412 bool have_hl; 413 size_t left_pad; 414 } pending_source_t; 415 416 static pending_source_t pending_source = { 0 }; 417 418 static void 419 request_source(int fd, const char *file, int64_t from, int64_t to, 420 const debug_highlight_span_t *hl, size_t left_pad) 421 { 422 struct json_object *payload; 423 424 /* A fetch for this exact file is already in flight (e.g. the initial 425 * PAUSED's own auto-context request hasn't resolved yet when a 426 * "lines" response also wants it) - don't send a second SOURCE 427 * request that would only overwrite this same pending_source's 428 * tracking with no way to reconcile the two, just adopt whichever 429 * range was asked for most recently and let the one response in 430 * flight satisfy it. */ 431 if (pending_source.active && !strcmp(pending_source.file, file)) { 432 pending_source.from = from; 433 pending_source.to = to; 434 pending_source.have_hl = (hl != NULL); 435 pending_source.left_pad = left_pad; 436 437 if (hl) 438 pending_source.hl = *hl; 439 440 return; 441 } 442 443 payload = json_object_new_object(); 444 json_object_object_add(payload, "file", json_object_new_string(file)); 445 proto_write(fd, "SOURCE", payload); 446 json_object_put(payload); 447 448 free(pending_source.file); 449 pending_source.active = true; 450 pending_source.file = strdup(file); 451 pending_source.from = from; 452 pending_source.to = to; 453 pending_source.have_hl = (hl != NULL); 454 pending_source.left_pad = left_pad; 455 456 if (hl) 457 pending_source.hl = *hl; 458 } 459 460 /* Current terminal width, for the same wrap/pad behavior 461 * debug_highlight_print_source()'s ported original had via term_width(). 462 * Falls back to 80 columns when stdout isn't a tty (e.g. piped output). */ 463 static size_t 464 term_columns(void) 465 { 466 struct winsize w; 467 468 if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == 0 && w.ws_col > 0) 469 return w.ws_col; 470 471 return 80; 472 } 473 474 /* Print source lines [from, to] (1-based, inclusive) from `file`, shading 475 * the `hl` statement span if given. If the text isn't cached yet, 476 * asynchronously requests it (see `pending_source` above) and returns 477 * without printing anything - the main loop's SOURCE response handler 478 * re-invokes this once the text has actually arrived. */ 479 480 /* Mirrors format_context_statement()'s range-splitting for a statement/ 481 * function too long to show in full: a window of context around `from`, 482 * a gap, and a window around the current instruction and/or `to` - using 483 * the same 2-line-before/2-line-after context radius render_paused()/ 484 * render_backtrace_final() already use. Returns the number of ranges 485 * written to `ranges` (1 if the span is short enough to just show whole, 486 * up to 3 otherwise). Falls back to a single [from, to] range verbatim if 487 * there's no known "current line" to anchor the split around. */ 488 static size_t 489 compute_context_ranges(int64_t from, int64_t to, const debug_highlight_span_t *hl, 490 debug_highlight_range_t ranges[3]) 491 { 492 const int64_t ctx = 2; 493 int64_t ip; 494 debug_highlight_range_t r[3] = { { 0, 0 }, { 0, 0 }, { 0, 0 } }; 495 size_t n = 0, i; 496 497 if (from < 1) 498 from = 1; 499 500 if (!hl || !hl->have_ip || to - from <= 4) { 501 ranges[0] = (debug_highlight_range_t){ (size_t)from, (size_t)to }; 502 return 1; 503 } 504 505 ip = (int64_t)hl->ip_line; 506 507 if (ip < from) 508 ip = from; 509 510 if (ip > to) 511 ip = to; 512 513 if (ip - from <= (ctx + ctx + 2)) { 514 r[1].from = (size_t)from; 515 } 516 else { 517 r[0].from = (size_t)from; 518 r[0].to = (size_t)(from + ctx); 519 r[1].from = (size_t)(ip - ctx); 520 } 521 522 if (to - ip <= (ctx + ctx + 2)) { 523 r[1].to = (size_t)to; 524 } 525 else { 526 r[1].to = (size_t)(ip + ctx); 527 r[2].from = (size_t)(to - ctx); 528 r[2].to = (size_t)to; 529 } 530 531 for (i = 0; i < 3; i++) 532 if (r[i].from && r[i].to) 533 ranges[n++] = r[i]; 534 535 return n; 536 } 537 538 static void 539 render_source_lines(int fd, const char *file, int64_t from, int64_t to, 540 const debug_highlight_span_t *hl, size_t left_pad) 541 { 542 size_t nlines; 543 char **lines = find_cached_source(file, &nlines); 544 545 if (!lines) 546 lines = try_load_local_file(file, &nlines); 547 548 if (!lines) { 549 request_source(fd, file, from, to, hl, left_pad); 550 return; 551 } 552 553 if (from < 1) 554 from = 1; 555 556 { 557 size_t columns = term_columns(); 558 debug_highlight_range_t ranges[3]; 559 size_t nranges; 560 561 columns = (columns > left_pad) ? columns - left_pad : 0; 562 nranges = compute_context_ranges(from, to, hl, ranges); 563 564 debug_highlight_print_source_ranges(stdout, lines, nlines, 565 nranges, ranges, hl, left_pad, columns); 566 } 567 } 568 569 /* -- response rendering ------------------------------------------------- */ 570 571 /* Join a JSON array of strings with " \xc2\xbb " (U+00BB, " ยป "), matching 572 * format_context_breadcrumb()'s separator. Caller frees the result. */ 573 static char * 574 join_breadcrumb(struct json_object *arr) 575 { 576 static const char sep[] = " \xc2\xbb "; /* U+00BB RIGHT-POINTING GUILLEMET */ 577 size_t n = arr ? json_object_array_length(arr) : 0; 578 size_t len = 0, i; 579 char *out, *p; 580 581 if (n == 0) 582 return strdup(""); 583 584 for (i = 0; i < n; i++) 585 len += strlen(json_object_get_string(json_object_array_get_idx(arr, i))); 586 587 len += (n - 1) * (sizeof(sep) - 1); 588 out = p = malloc(len + 1); 589 590 for (i = 0; i < n; i++) { 591 const char *s = json_object_get_string(json_object_array_get_idx(arr, i)); 592 size_t l = strlen(s); 593 594 if (i > 0) { 595 memcpy(p, sep, sizeof(sep) - 1); 596 p += sizeof(sep) - 1; 597 } 598 599 memcpy(p, s, l); 600 p += l; 601 } 602 603 *p = '\0'; 604 605 return out; 606 } 607 608 static void 609 render_paused(int fd, struct json_object *p) 610 { 611 int64_t line = jint(p, "line", 0); 612 const char *file = jstr(p, "file", NULL); 613 614 printf(C_BOLD "Paused" C_RESET " (%s) in " C_BOLD "%s()" C_RESET ", %s:%" PRId64 ":%" PRId64 "\n", 615 jstr(p, "reason", "?"), 616 jstr(p, "function", "?"), 617 file ? file : "?", 618 line, 619 jint(p, "col", 0)); 620 621 if (json_object_object_get_ex(p, "breakpoint_id", NULL)) 622 printf(" " C_GREEN "breakpoint #%" PRId64 C_RESET "\n", jint(p, "breakpoint_id", 0)); 623 624 if (json_object_object_get_ex(p, "exception_message", NULL)) 625 printf(" " C_RED "exception: %s" C_RESET "\n", jstr(p, "exception_message", "")); 626 627 if (file && line > 0) { 628 struct json_object *breadcrumb_arr = json_object_object_get(p, "breadcrumb"); 629 char *breadcrumb = join_breadcrumb(breadcrumb_arr); 630 int64_t col = jint(p, "col", 0); 631 debug_highlight_span_t hl = { 632 .from_line = (size_t)line, .from_col = 0, 633 .to_line = (size_t)line, .to_col = SIZE_MAX, 634 .have_ip = true, .ip_line = (size_t)line, .ip_col = col0(col) 635 }; 636 637 debug_highlight_print_header_bar(stdout, file, breadcrumb, 0, term_columns()); 638 free(breadcrumb); 639 640 render_source_lines(fd, file, line - 2, line + 2, &hl, 0); 641 } 642 } 643 644 static void 645 render_breakpoints(struct json_object *p) 646 { 647 struct json_object *items = NULL; 648 size_t i, n; 649 650 json_object_object_get_ex(p, "items", &items); 651 n = items ? json_object_array_length(items) : 0; 652 653 if (n == 0) { 654 printf("No breakpoints set\n"); 655 return; 656 } 657 658 for (i = 0; i < n; i++) { 659 struct json_object *it = json_object_array_get_idx(items, i); 660 struct json_object *idv = NULL; 661 662 if (json_object_object_get_ex(it, "id", &idv)) 663 printf(C_BOLD "#%-4" PRId64 C_RESET " ", json_object_get_int64(idv)); 664 else 665 printf(C_DIM "(%-4s)" C_RESET " ", jstr(it, "kind", "?")); 666 667 if (json_object_object_get_ex(it, "file", NULL)) 668 printf("%s:%" PRId64 ":%" PRId64 " - %s\n", 669 jstr(it, "file", "?"), jint(it, "line", 0), 670 jint(it, "col", 0), jstr(it, "function", "?")); 671 else 672 printf("<next instruction>\n"); 673 } 674 } 675 676 static void 677 render_variables_array(struct json_object *items, const char *indent) 678 { 679 size_t i, n = items ? json_object_array_length(items) : 0; 680 debug_variable_t *vars = calloc(n ? n : 1, sizeof(*vars)); 681 682 for (i = 0; i < n; i++) { 683 struct json_object *it = json_object_array_get_idx(items, i); 684 struct json_object *shadowed_j = json_object_object_get(it, "shadowed"); 685 686 vars[i].name = jstr(it, "name", "?"); 687 vars[i].kind = jstr(it, "kind", ""); 688 vars[i].value_repr = jstr(it, "value_repr", ""); 689 vars[i].shadowed = shadowed_j && json_object_get_boolean(shadowed_j); 690 } 691 692 debug_highlight_print_variables(stdout, vars, n, indent, term_columns()); 693 free(vars); 694 } 695 696 /* Async multi-file fetch for render_backtrace(): a backtrace can span 697 * several source files at once (unlike PAUSED/LINES, which only ever need 698 * one), so a single pending_source-style slot isn't enough - this instead 699 * queues every file the frames need that isn't cached yet, fetches them 700 * one at a time, and only actually prints once all of them have arrived. */ 701 typedef struct { 702 bool active; 703 struct json_object *payload; 704 char **files; 705 size_t nfiles, next; 706 } pending_backtrace_t; 707 708 static pending_backtrace_t pending_backtrace = { 0 }; 709 710 static void 711 request_backtrace_file(int fd, const char *file) 712 { 713 struct json_object *payload = json_object_new_object(); 714 715 json_object_object_add(payload, "file", json_object_new_string(file)); 716 proto_write(fd, "SOURCE", payload); 717 json_object_put(payload); 718 } 719 720 static void 721 render_backtrace_final(int fd, struct json_object *p) 722 { 723 struct json_object *frames = NULL; 724 size_t i, n; 725 726 json_object_object_get_ex(p, "frames", &frames); 727 n = frames ? json_object_array_length(frames) : 0; 728 729 for (i = 0; i < n; i++) { 730 struct json_object *fr = json_object_array_get_idx(frames, i); 731 struct json_object *vars = NULL; 732 const char *file = jstr(fr, "file", NULL); 733 int64_t line = jint(fr, "line", 0); 734 int64_t col = jint(fr, "col", 0); 735 bool native = !strcmp(jstr(fr, "kind", ""), "native"); 736 char signature[256]; 737 738 char prefix[16]; 739 size_t prefix_len, columns = term_columns(); 740 741 snprintf(signature, sizeof(signature), "%s()", jstr(fr, "function", "?")); 742 743 /* "#N " is printed right before the header bar, on the same line - 744 * left_pad itself would make the bar draw *another* copy of that 745 * indentation (it is meant for a bar that draws its own leading 746 * blanks, see render_paused() above for that usage), so instead 747 * just shrink the width budget by the prefix that already went 748 * out via printf() below, and leave left_pad at 0. Without this, 749 * the bar is sized for the full terminal width and the combined 750 * line overflows it by exactly the prefix's length. */ 751 prefix_len = (size_t)snprintf(prefix, sizeof(prefix), "#%-2" PRId64 " ", jint(fr, "index", 0)); 752 columns = (columns > prefix_len) ? columns - prefix_len : 0; 753 754 printf(C_BOLD "%s" C_RESET, prefix); 755 756 debug_highlight_print_header_bar(stdout, 757 native ? "C" : (file ? file : "?"), signature, 0, columns); 758 759 if (!native && file && line > 0) { 760 debug_highlight_span_t hl = { 761 .from_line = (size_t)line, .from_col = 0, 762 .to_line = (size_t)line, .to_col = SIZE_MAX, 763 .have_ip = true, .ip_line = (size_t)line, .ip_col = col0(col) 764 }; 765 766 render_source_lines(fd, file, line - 2, line + 2, &hl, 2); 767 } 768 769 /* tail call optimization reuses this frame for the callee, so the 770 * intermediate frames of the tail calls are absent from the stack - 771 * show the gap (see uc_vm_frame_reinit() in vm.c). */ 772 { 773 struct json_object *tco_j = json_object_object_get(fr, "tco"); 774 775 if (tco_j && json_object_get_int64(tco_j) > 0) 776 printf(C_DIM " (%" PRId64 " tail call frames omitted)" C_RESET "\n", 777 json_object_get_int64(tco_j)); 778 } 779 780 if (json_object_object_get_ex(fr, "variables", &vars)) 781 render_variables_array(vars, " - "); 782 783 printf("\n"); 784 } 785 } 786 787 static void 788 render_backtrace(int fd, struct json_object *p) 789 { 790 struct json_object *frames = NULL; 791 size_t i, n; 792 char **missing = NULL; 793 size_t n_missing = 0, cap = 0; 794 795 json_object_object_get_ex(p, "frames", &frames); 796 n = frames ? json_object_array_length(frames) : 0; 797 798 for (i = 0; i < n; i++) { 799 struct json_object *fr = json_object_array_get_idx(frames, i); 800 const char *file = jstr(fr, "file", NULL); 801 size_t dummy; 802 size_t j; 803 bool already = false; 804 805 if (!file || strcmp(jstr(fr, "kind", ""), "script")) 806 continue; 807 808 if (find_cached_source(file, &dummy)) 809 continue; 810 811 if (try_load_local_file(file, &dummy)) 812 continue; 813 814 for (j = 0; j < n_missing; j++) 815 if (!strcmp(missing[j], file)) 816 already = true; 817 818 if (already) 819 continue; 820 821 if (n_missing >= cap) { 822 cap = cap ? cap * 2 : 4; 823 missing = realloc(missing, cap * sizeof(*missing)); 824 } 825 826 missing[n_missing++] = strdup(file); 827 } 828 829 if (n_missing == 0) { 830 free(missing); 831 render_backtrace_final(fd, p); 832 return; 833 } 834 835 pending_backtrace.active = true; 836 pending_backtrace.payload = json_object_get(p); 837 pending_backtrace.files = missing; 838 pending_backtrace.nfiles = n_missing; 839 pending_backtrace.next = 0; 840 841 request_backtrace_file(fd, missing[0]); 842 } 843 844 static void 845 render_source_range(int fd, struct json_object *p) 846 { 847 const char *file = jstr(p, "file", NULL); 848 struct json_object *cursor = json_object_object_get(p, "cursor"); 849 int64_t from = jint(p, "from", 0); 850 int64_t to = jint(p, "to", 0); 851 debug_highlight_span_t hl; 852 853 if (!file) { 854 printf("(no source range)\n"); 855 return; 856 } 857 858 if (cursor) { 859 hl.from_line = (size_t)jint(cursor, "from_line", 0); 860 hl.from_col = col0(jint(cursor, "from_col", 0)); 861 hl.to_line = (size_t)jint(cursor, "to_line", 0); 862 hl.to_col = col0(jint(cursor, "to_col", 0)); 863 864 /* The protocol only gives us the statement's *span*, not the 865 * exact current instruction position within it (which can differ 866 * for multi-part expressions) - approximate with the span start, 867 * which is exact for the common case of a simple statement. */ 868 hl.have_ip = true; 869 hl.ip_line = hl.from_line; 870 hl.ip_col = hl.from_col; 871 872 render_source_lines(fd, file, from, to, &hl, 0); 873 } 874 else { 875 render_source_lines(fd, file, from, to, NULL, 0); 876 } 877 } 878 879 /* Fill in the raw byte array fields of a debug_disasm_insn_t (or a 880 * capture/unpack sub-entry) from a JSON array of small integers. `dst` must 881 * already point at storage for at least `cap` bytes; only the first 882 * min(array length, cap) entries are filled. */ 883 static void 884 jbytes(struct json_object *arr, unsigned char *dst, size_t cap) 885 { 886 size_t n = arr ? json_object_array_length(arr) : 0; 887 888 if (n > cap) 889 n = cap; 890 891 for (size_t i = 0; i < n; i++) 892 dst[i] = (unsigned char)json_object_get_int64(json_object_array_get_idx(arr, i)); 893 } 894 895 static void 896 render_disassembly(struct json_object *p) 897 { 898 struct json_object *insns_j = NULL; 899 debug_disasm_insn_t *insns; 900 size_t n; 901 902 json_object_object_get_ex(p, "instructions", &insns_j); 903 n = insns_j ? json_object_array_length(insns_j) : 0; 904 insns = calloc(n, sizeof(*insns)); 905 906 for (size_t i = 0; i < n; i++) { 907 struct json_object *ins = json_object_array_get_idx(insns_j, i); 908 struct json_object *bytes_j = json_object_object_get(ins, "bytes"); 909 struct json_object *constant_j = NULL; 910 struct json_object *captures_j = json_object_object_get(ins, "captures"); 911 struct json_object *unpacks_j = json_object_object_get(ins, "unpacks"); 912 debug_disasm_insn_t *d = &insns[i]; 913 size_t nbytes = bytes_j ? json_object_array_length(bytes_j) : 0; 914 unsigned char *bytes = malloc(nbytes ? nbytes : 1); 915 916 jbytes(bytes_j, bytes, nbytes); 917 918 d->offset = (size_t)jint(ins, "offset", 0); 919 d->mnemonic = jstr(ins, "mnemonic", "?"); 920 d->format = (int)jint(ins, "format", 0); 921 d->bytes = bytes; 922 d->nbytes = nbytes; 923 d->operand = jint(ins, "operand", 0); 924 925 if (json_object_object_get_ex(ins, "constant", &constant_j)) { 926 d->have_constant = true; 927 d->constant_repr = json_object_to_json_string(constant_j); 928 d->constant_is_string = (json_object_get_type(constant_j) == json_type_string); 929 } 930 931 if (json_object_object_get_ex(ins, "variable_kind", NULL)) { 932 d->variable_kind = jstr(ins, "variable_kind", NULL); 933 d->variable_name = jstr(ins, "variable_name", NULL); 934 } 935 936 if (json_object_object_get_ex(ins, "closure_index", NULL)) { 937 d->have_closure = true; 938 d->closure_kind = jstr(ins, "closure_kind", "closure"); 939 d->closure_index = (uint32_t)jint(ins, "closure_index", 0); 940 } 941 942 if (json_object_object_get_ex(ins, "call_nargs", NULL)) { 943 struct json_object *mcall_j = json_object_object_get(ins, "call_mcall"); 944 struct json_object *tail_j = json_object_object_get(ins, "call_tail"); 945 946 d->have_call = true; 947 d->call_mcall = mcall_j && json_object_get_boolean(mcall_j); 948 d->call_nargs = (uint32_t)jint(ins, "call_nargs", 0); 949 d->call_tail = tail_j && json_object_get_boolean(tail_j); 950 } 951 952 if (json_object_object_get_ex(ins, "return_tailcall", NULL)) 953 d->return_tailcall = true; 954 955 d->ncaptures = captures_j ? json_object_array_length(captures_j) : 0; 956 d->captures = calloc(d->ncaptures ? d->ncaptures : 1, sizeof(*d->captures)); 957 958 for (size_t j = 0; j < d->ncaptures; j++) { 959 struct json_object *cap = json_object_array_get_idx(captures_j, j); 960 961 d->captures[j].slot = jint(cap, "slot", 0); 962 d->captures[j].upval = !strcmp(jstr(cap, "kind", ""), "upval"); 963 d->captures[j].name = jstr(cap, "name", "(unknown)"); 964 jbytes(json_object_object_get(cap, "bytes"), d->captures[j].bytes, 4); 965 } 966 967 d->nunpacks = unpacks_j ? json_object_array_length(unpacks_j) : 0; 968 d->unpacks = calloc(d->nunpacks ? d->nunpacks : 1, sizeof(*d->unpacks)); 969 970 for (size_t j = 0; j < d->nunpacks; j++) { 971 struct json_object *u = json_object_array_get_idx(unpacks_j, j); 972 973 d->unpacks[j].slot = (uint16_t)jint(u, "slot", 0); 974 jbytes(json_object_object_get(u, "bytes"), d->unpacks[j].bytes, 2); 975 } 976 } 977 978 debug_highlight_print_disassembly(stdout, jstr(p, "function", "?"), insns, n, term_columns()); 979 980 for (size_t i = 0; i < n; i++) { 981 free((void *)insns[i].bytes); 982 free(insns[i].captures); 983 free(insns[i].unpacks); 984 } 985 986 free(insns); 987 } 988 989 /* Async server events (see EVENT in lib/debug_proto.h) can land at any 990 * time, unprompted by anything the user typed - set in a faint italic 991 * style to visually set them apart from direct command responses. */ 992 static void 993 render_event(struct json_object *p) 994 { 995 const char *event = jstr(p, "event", "?"); 996 997 printf(C_EVENT); 998 999 if (!strcmp(event, "exception")) { 1000 struct json_object *exc = json_object_object_get(p, "exception"); 1001 1002 printf("*** exception: %s: %s ***", jstr(exc, "type", "Error"), 1003 jstr(exc, "message", "?")); 1004 } 1005 else if (!strcmp(event, "exit")) { 1006 const char *status = jstr(p, "status", "?"); 1007 1008 if (!strcmp(status, "OK")) { 1009 printf("*** program finished ***"); 1010 } 1011 else if (!strcmp(status, "EXIT")) { 1012 printf("*** program exited (code %" PRId64 ") ***", jint(p, "code", 0)); 1013 } 1014 else { 1015 struct json_object *exc = json_object_object_get(p, "exception"); 1016 1017 if (exc) 1018 printf("*** program terminated: %s: %s ***", 1019 jstr(exc, "type", "Error"), jstr(exc, "message", "?")); 1020 else 1021 printf("*** program terminated (%s) ***", status); 1022 } 1023 } 1024 else { 1025 printf("*** event: %s %s ***", event, 1026 json_object_to_json_string_ext(p, JSON_C_TO_STRING_SPACED)); 1027 } 1028 1029 printf(C_RESET "\n"); 1030 } 1031 1032 static void 1033 render_response(int fd, const char *verb, struct json_object *payload) 1034 { 1035 if (!strcmp(verb, "PAUSED")) 1036 render_paused(fd, payload); 1037 else if (!strcmp(verb, "BREAKPOINTS")) 1038 render_breakpoints(payload); 1039 else if (!strcmp(verb, "VARIABLES")) 1040 render_variables_array(json_object_object_get(payload, "vars"), ""); 1041 else if (!strcmp(verb, "BACKTRACE")) 1042 render_backtrace(fd, payload); 1043 else if (!strcmp(verb, "SOURCE_RANGE")) 1044 render_source_range(fd, payload); 1045 else if (!strcmp(verb, "DISASSEMBLY")) 1046 render_disassembly(payload); 1047 else if (!strcmp(verb, "ERROR")) 1048 printf(C_RED "Error: %s" C_RESET "\n", jstr(payload, "message", "(unknown error)")); 1049 else if (!strcmp(verb, "VALUE")) 1050 printf("%s\n", jstr(payload, "repr", "")); 1051 else if (!strcmp(verb, "BREAKPOINT_ADDED")) 1052 printf(C_GREEN "Breakpoint #%" PRId64 " added" C_RESET "\n", jint(payload, "id", 0)); 1053 else if (!strcmp(verb, "EVENT")) 1054 render_event(payload); 1055 else if (!strcmp(verb, "SOURCE")) { 1056 const char *text = jstr(payload, "text", NULL); 1057 const char *file = jstr(payload, "file", "?"); 1058 1059 if (text) { 1060 size_t nlines; 1061 1062 cache_source_text(file, text, &nlines); 1063 1064 /* A render_backtrace() multi-file fetch takes priority: advance 1065 * its queue and either request the next missing file or, once 1066 * every frame's file is cached, finally print the whole thing. */ 1067 if (pending_backtrace.active && pending_backtrace.next < pending_backtrace.nfiles && 1068 !strcmp(pending_backtrace.files[pending_backtrace.next], file)) { 1069 pending_backtrace.next++; 1070 1071 if (pending_backtrace.next < pending_backtrace.nfiles) { 1072 request_backtrace_file(fd, pending_backtrace.files[pending_backtrace.next]); 1073 } 1074 else { 1075 size_t i; 1076 1077 render_backtrace_final(fd, pending_backtrace.payload); 1078 json_object_put(pending_backtrace.payload); 1079 1080 for (i = 0; i < pending_backtrace.nfiles; i++) 1081 free(pending_backtrace.files[i]); 1082 1083 free(pending_backtrace.files); 1084 pending_backtrace = (pending_backtrace_t){ 0 }; 1085 } 1086 } 1087 /* Finishing an auto-fetch triggered by render_paused()/ 1088 * render_source_range() (see pending_source) is distinct from 1089 * a direct response to a user-typed "source <file>" command: 1090 * the former re-renders exactly the range that was originally 1091 * requested, the latter shows the whole file. */ 1092 else if (pending_source.active && !strcmp(pending_source.file, file)) { 1093 int64_t from = pending_source.from; 1094 int64_t to = pending_source.to; 1095 bool have_hl = pending_source.have_hl; 1096 debug_highlight_span_t hl = pending_source.hl; 1097 size_t left_pad = pending_source.left_pad; 1098 1099 pending_source.active = false; 1100 render_source_lines(fd, file, from, to, have_hl ? &hl : NULL, left_pad); 1101 } 1102 else { 1103 printf("--- %s ---\n", file); 1104 render_source_lines(fd, file, 1, (int64_t)nlines, NULL, 0); 1105 } 1106 } 1107 else { 1108 printf("(source unavailable: %s)\n", jstr(payload, "error", "?")); 1109 pending_source.active = false; 1110 1111 if (pending_backtrace.active) { 1112 size_t i; 1113 1114 /* Missing source for one frame shouldn't block showing the 1115 * rest - just print what we have (unavailable files will 1116 * fall back to "no snippet" for that frame). */ 1117 render_backtrace_final(fd, pending_backtrace.payload); 1118 json_object_put(pending_backtrace.payload); 1119 1120 for (i = 0; i < pending_backtrace.nfiles; i++) 1121 free(pending_backtrace.files[i]); 1122 1123 free(pending_backtrace.files); 1124 pending_backtrace = (pending_backtrace_t){ 0 }; 1125 } 1126 } 1127 } 1128 else if (!strcmp(verb, "HELP")) { 1129 struct json_object *cmds = json_object_object_get(payload, "commands"); 1130 size_t i, n = cmds ? json_object_array_length(cmds) : 0; 1131 1132 for (i = 0; i < n; i++) { 1133 struct json_object *c = json_object_array_get_idx(cmds, i); 1134 1135 printf("%-16s %s\n", jstr(c, "verb", "?"), jstr(c, "help", "")); 1136 } 1137 } 1138 else if (!strcmp(verb, "SOURCES")) { 1139 struct json_object *items = json_object_object_get(payload, "items"); 1140 size_t i, n = items ? json_object_array_length(items) : 0; 1141 1142 for (i = 0; i < n; i++) { 1143 struct json_object *it = json_object_array_get_idx(items, i); 1144 1145 printf("#%-2" PRId64 " %s\n", jint(it, "index", 0), jstr(it, "file", "?")); 1146 } 1147 } 1148 else if (!strcmp(verb, "OK")) { 1149 printf("OK\n"); 1150 } 1151 else if (!strcmp(verb, "RESUME")) { 1152 printf("(resumed)\n"); 1153 } 1154 else if (payload) { 1155 printf("%s %s\n", verb, json_object_to_json_string_ext(payload, JSON_C_TO_STRING_SPACED)); 1156 } 1157 else { 1158 printf("%s\n", verb); 1159 } 1160 } 1161 1162 /* -- typed command line -> VERB {payload} translation -------------------- */ 1163 1164 static char * 1165 trim(char *s) 1166 { 1167 char *end; 1168 1169 while (isspace((unsigned char)*s)) 1170 s++; 1171 1172 end = s + strlen(s); 1173 1174 while (end > s && isspace((unsigned char)end[-1])) 1175 *--end = '\0'; 1176 1177 return s; 1178 } 1179 1180 /* Split off the first whitespace-delimited word from *rest, returning it and 1181 * advancing *rest to the remainder (leading space trimmed). */ 1182 static char * 1183 shift_word(char **rest) 1184 { 1185 char *p = *rest; 1186 char *word; 1187 1188 while (isspace((unsigned char)*p)) 1189 p++; 1190 1191 word = p; 1192 1193 while (*p && !isspace((unsigned char)*p)) 1194 p++; 1195 1196 if (*p) { 1197 *p = '\0'; 1198 p++; 1199 1200 while (isspace((unsigned char)*p)) 1201 p++; 1202 } 1203 1204 *rest = p; 1205 1206 return word; 1207 } 1208 1209 /* True if `typed` is a non-empty prefix of any of the NUL-separated names 1210 * in `names` (e.g. "list\0ls\0") - shortest-unique-prefix command matching, 1211 * same as the original interactive CLI's `commands[]` dispatch. Ambiguous 1212 * prefixes (matching more than one command) resolve to whichever command 1213 * is checked first below, in the same fixed order the original table 1214 * declared them in. */ 1215 static bool 1216 match_cmd(const char *names, const char *typed) 1217 { 1218 size_t typed_len = strlen(typed); 1219 const char *p = names; 1220 1221 if (typed_len == 0) 1222 return false; 1223 1224 while (*p) { 1225 size_t len = strlen(p); 1226 1227 if (len >= typed_len && !strncmp(p, typed, typed_len)) 1228 return true; 1229 1230 p += len + 1; 1231 } 1232 1233 return false; 1234 } 1235 1236 /* CLI usage documentation, ported verbatim from the pre-protocol interactive 1237 * debugger's `commands[]`/cmd_help() (formerly lib/debug.c) - this describes 1238 * *this client's* typed command syntax, so unlike everything else in this 1239 * file it is never fetched from the server: the server's own HELP verb 1240 * answers a different question (the wire protocol's verbs and payload 1241 * shapes, for anything else that might speak the protocol directly) and 1242 * showing that to an interactive user here just reads as raw protocol 1243 * internals. `names` is a NUL-separated list of aliases, primary name 1244 * first - match_cmd() already implements the exact prefix-matching lookup 1245 * this needs, so it is reused here for `help <partial-name>` filtering. */ 1246 static const struct { 1247 const char *names; 1248 const char *help; 1249 } cli_help_table[] = { 1250 { "help\0h\0?\0", 1251 "Print help information." }, 1252 { "break\0b\0", 1253 "The break command sets a breakpoint at the given location, " 1254 "instructing the virtual machine to stop execution at this " 1255 "point and handing control to the debugger.\n\n" 1256 "Breakpoint locations may be specified either as filename, " 1257 "line number and optional character offset within the line " 1258 "or as a ucode expression that evaluates to a function in " 1259 "which a breakpoint is set.\n\n" 1260 "Examples:\n" 1261 " break example.uc:13 # Set breakpoint in line 13 of example.uc\n" 1262 " break 4:17 # Break in line in 4, char 17 of current file\n" 1263 " break myobj.method # Break in function `method` of `myobj`\n" 1264 " break (string.uc) # Parens to disambiguate expression from path" 1265 }, 1266 { "delete\0d\0", 1267 "Delete a breakpoint. When no argument is given, the current " 1268 "breakpoint is deleted, otherwise this function deletes the breakpoint " 1269 "with the given index.\n\n" 1270 "Examples:\n" 1271 " delete # Delete current breakpoint\n" 1272 " delete 2 # Delete breakpoint #2" 1273 }, 1274 { "list\0ls\0", 1275 "List all currently set breakpoints. User defined breakpoints are " 1276 "prefixed with a number identifying the breakpoint, internal " 1277 "breakpoints used by the debugger are prefixed with a breakpoint type " 1278 "enclosed in parens, e.g. '(step)'." 1279 }, 1280 { "next\0n\0", 1281 "Execute the next statement and stop again." 1282 }, 1283 { "step\0s\0", 1284 "Execute the next statement, in case of function calls step into the " 1285 "called function and stop there." 1286 }, 1287 { "continue\0c\0", 1288 "Continue execution until the next breakpoint or end of program." 1289 }, 1290 { "return\0", 1291 "Continue executing the current function until it returns, then stop " 1292 "in the calling function. If the current function is the program entry " 1293 "function, then run until the end of the program." 1294 }, 1295 { "backtrace\0bt\0", 1296 "Print a trace of the current callstack, with most recent callframes " 1297 "output first. If the optional 'full' argument is specified, " 1298 "additional information about each call frame is printed.\n\n" 1299 "Examples:\n" 1300 " backtrace # Print backtrace\n" 1301 " backtrace full # Print backtrace with additional information" 1302 }, 1303 { "variables\0vars\0", 1304 "Print local variables and their contents for the current execution " 1305 "context. Internal variables which are unreachable by script code " 1306 "are shown faint, upvalues (variables captured from parent scopes) " 1307 "are shown in bold cyan and ordinary variables use the default color.\n\n" 1308 "Examples:\n" 1309 " variables # Print local variables" 1310 }, 1311 { "sources\0src\0", 1312 "Print a list of loaded source buffers." 1313 }, 1314 { "print\0p\0", 1315 "Evaluate an ucode expression and print the resulting value - like " 1316 "the ucode CLI's `-p`.\n\n" 1317 "Examples:\n" 1318 " print varname # Print value of variable 'varname'\n" 1319 " print myobj.prop # Print `prop` property of `myobj`\n" 1320 " print keys(myobj) # Invoke a stdlib function" 1321 }, 1322 { "eval\0e\0", 1323 "Evaluate an ucode expression, discarding its result instead of " 1324 "printing it - like the ucode CLI's `-e`. The idiomatic way to " 1325 "change a variable's value while paused: assignment is just " 1326 "ordinary expression syntax, so a plain variable, a property path " 1327 "or an array index all work the same way a script would write " 1328 "them, without a separate dedicated command for it.\n\n" 1329 "Examples:\n" 1330 " eval x = 5 # Assign the number 5 to variable 'x'\n" 1331 " eval x.y = 1 # Assign 1 to property 'y' of 'x'\n" 1332 " eval delete foo.bar # Delete property 'bar' of 'foo'" 1333 }, 1334 { "lines\0ln\0", 1335 "Print source code lines surrounding the given location specified " 1336 "either as filename with line number or as expression evaluating to a " 1337 "function value.\n\n" 1338 "The amount of preceding and following lines to print may be " 1339 "specified as second and third argument respectively. By default, two " 1340 "lines of context are printed before and after the location.\n\n" 1341 "Examples:\n" 1342 " lines # Output lines surrounding current line\n" 1343 " lines example.uc # Print first three lines of example.uc\n" 1344 " lines (obj.func) # Parens to disambiguate expression from path\n" 1345 " lines foo 5 8 # Print 5 lines before foo() till 8 lines in\n" 1346 " lines #123 # Print source of instruction offset 123\n" 1347 " lines +0 3 3 # Print 3 lines before and after current line\n" 1348 " lines -5 # Print source 5 lines before current line\n" 1349 " lines +3 # Print source 3 lines after current line" 1350 }, 1351 { "throw\0", 1352 "Raise an exception at the current instruction offset.\n\n" 1353 "Examples:\n" 1354 " throw \"Message\" # Throw exception with given message" 1355 }, 1356 { "disassemble\0disasm\0", 1357 "Disassemble the given function or statement location and output the " 1358 "corresponding byte code in a human readable manner. The location to " 1359 "disassemble may be either a function name, a single instruction " 1360 "offset, an instruction offset range or a ucode expression.\n\n" 1361 "Examples:\n" 1362 " disassemble # Disassemble current statment\n" 1363 " disassemble foo # Disassemble body of foo()\n" 1364 " disassemble foo+100 # Disassemble first 100 byte of function foo()\n" 1365 " disassemble #5 # Disassemble statement containing instruction 5\n" 1366 " disassemble #2-10 # Disassemble instructions 2 to 10\n" 1367 " disassemble #22+100 # Disassemble instructions 22 to 122\n" 1368 " disassemble (12/3*4) # Disassemble ucode expression" 1369 }, 1370 { "source\0", 1371 "Fetch and print the raw source text the server has for a file path, " 1372 "without syntax highlighting - mostly useful to check exactly what " 1373 "the server sees when it differs from the local copy." 1374 }, 1375 { "quit\0q\0", 1376 "Forcibly terminate the currently running program. The termination " 1377 "happens in the same manner as if 'exit()' has been called from " 1378 "script code." 1379 }, 1380 }; 1381 1382 /* Word-wrap and print one help entry's body to `columns`, preserving 1383 * existing line breaks (so an "Examples:" block's indentation survives) 1384 * and paragraph gaps - ported verbatim from cmd_help(), formerly 1385 * lib/debug.c, with term_printf()/term_print() replaced by printf(). */ 1386 static void 1387 print_help_entry(const char *names, const char *help, size_t columns) 1388 { 1389 const char *p = help; 1390 1391 printf(C_BOLD "%s" C_RESET "\n\n", names); 1392 1393 while (*p != '\0') { 1394 size_t pad = strspn(p, " "); 1395 size_t len = strcspn(p, "\r\n") - pad; 1396 1397 if (pad + len <= columns) { 1398 printf("%.*s\n", (int)(pad + len), p); 1399 p += pad + len + (p[pad + len] == '\n'); 1400 } 1401 else { 1402 if (pad > columns) 1403 pad = 1; 1404 1405 const char *l = p + pad; 1406 1407 while (len > columns - pad) { 1408 printf("%.*s", (int)pad, p); 1409 1410 for (size_t j = columns - pad; j > 0; j--) { 1411 if (l[j - 1] == ' ') { 1412 printf("%.*s\n", (int)j, l); 1413 l += j; 1414 len -= j; 1415 break; 1416 } 1417 } 1418 } 1419 1420 printf("%.*s", (int)pad, p); 1421 printf("%.*s\n", (int)len, l); 1422 p = l + len + (l[len] == '\n'); 1423 } 1424 } 1425 1426 printf("\n\n"); 1427 } 1428 1429 static void 1430 print_help(const char *cmd) 1431 { 1432 size_t columns = term_columns(); 1433 size_t n = sizeof(cli_help_table) / sizeof(cli_help_table[0]); 1434 1435 for (size_t i = 0; i < n; i++) { 1436 if (cmd && *cmd && !match_cmd(cli_help_table[i].names, cmd)) 1437 continue; 1438 1439 print_help_entry(cli_help_table[i].names, cli_help_table[i].help, columns); 1440 } 1441 } 1442 1443 static bool 1444 send_command(int fd, char *line, bool *resuming, bool *sent) 1445 { 1446 char *cmd = shift_word(&line); 1447 struct json_object *payload = NULL; 1448 1449 *resuming = false; 1450 *sent = true; 1451 1452 if (!*cmd) { 1453 *sent = false; 1454 return true; 1455 } 1456 1457 if (match_cmd("help\0h\0?\0", cmd)) { 1458 print_help(*line ? line : NULL); 1459 *sent = false; 1460 } 1461 else if (match_cmd("break\0b\0", cmd)) { 1462 payload = json_object_new_object(); 1463 json_object_object_add(payload, "spec", json_object_new_string(line)); 1464 proto_write(fd, "BREAK", payload); 1465 } 1466 else if (match_cmd("delete\0d\0", cmd)) { 1467 if (*line) { 1468 payload = json_object_new_object(); 1469 json_object_object_add(payload, "id", json_object_new_int64(strtoll(line, NULL, 10))); 1470 } 1471 1472 proto_write(fd, "DELETE", payload); 1473 } 1474 else if (match_cmd("list\0ls\0", cmd)) { 1475 proto_write(fd, "LIST_BREAKPOINTS", NULL); 1476 } 1477 else if (match_cmd("next\0n\0", cmd)) { 1478 proto_write(fd, "NEXT", NULL); 1479 *resuming = true; 1480 } 1481 else if (match_cmd("step\0s\0", cmd)) { 1482 proto_write(fd, "STEP", NULL); 1483 *resuming = true; 1484 } 1485 else if (match_cmd("continue\0c\0", cmd)) { 1486 proto_write(fd, "CONTINUE", NULL); 1487 *resuming = true; 1488 } 1489 else if (match_cmd("return\0", cmd)) { 1490 proto_write(fd, "RETURN", NULL); 1491 *resuming = true; 1492 } 1493 else if (match_cmd("backtrace\0bt\0", cmd)) { 1494 payload = json_object_new_object(); 1495 json_object_object_add(payload, "full", 1496 json_object_new_boolean(!strcmp(trim(line), "full"))); 1497 proto_write(fd, "BACKTRACE", payload); 1498 } 1499 else if (match_cmd("variables\0vars\0", cmd)) { 1500 proto_write(fd, "VARIABLES", NULL); 1501 } 1502 else if (match_cmd("sources\0src\0", cmd)) { 1503 proto_write(fd, "SOURCES", NULL); 1504 } 1505 else if (match_cmd("print\0p\0", cmd)) { 1506 payload = json_object_new_object(); 1507 json_object_object_add(payload, "expr", json_object_new_string(line)); 1508 proto_write(fd, "PRINT", payload); 1509 } 1510 else if (match_cmd("eval\0e\0", cmd)) { 1511 payload = json_object_new_object(); 1512 json_object_object_add(payload, "expr", json_object_new_string(line)); 1513 proto_write(fd, "EVAL", payload); 1514 } 1515 else if (match_cmd("lines\0ln\0", cmd)) { 1516 char *spec = shift_word(&line); 1517 char *before = shift_word(&line); 1518 char *after = shift_word(&line); 1519 1520 payload = json_object_new_object(); 1521 1522 if (*spec) 1523 json_object_object_add(payload, "spec", json_object_new_string(spec)); 1524 1525 if (*before) 1526 json_object_object_add(payload, "before", json_object_new_int64(strtoll(before, NULL, 10))); 1527 1528 if (*after) 1529 json_object_object_add(payload, "after", json_object_new_int64(strtoll(after, NULL, 10))); 1530 1531 proto_write(fd, "LINES", payload); 1532 } 1533 else if (match_cmd("throw\0", cmd)) { 1534 char *first = shift_word(&line); 1535 static const char *types[] = { 1536 "syntax", "runtime", "type", "reference", "user", "exit" 1537 }; 1538 size_t i; 1539 bool is_type = false; 1540 1541 for (i = 0; i < sizeof(types) / sizeof(types[0]); i++) { 1542 if (!strncmp(types[i], first, strlen(first))) { 1543 is_type = true; 1544 break; 1545 } 1546 } 1547 1548 payload = json_object_new_object(); 1549 1550 if (is_type && *line) { 1551 json_object_object_add(payload, "type", json_object_new_string(first)); 1552 json_object_object_add(payload, "message", json_object_new_string(line)); 1553 } 1554 else { 1555 char *msg = *line ? line : first; 1556 1557 json_object_object_add(payload, "message", json_object_new_string(msg)); 1558 } 1559 1560 proto_write(fd, "THROW", payload); 1561 } 1562 else if (match_cmd("disassemble\0disasm\0", cmd)) { 1563 if (*line) { 1564 payload = json_object_new_object(); 1565 json_object_object_add(payload, "spec", json_object_new_string(line)); 1566 } 1567 1568 proto_write(fd, "DISASSEMBLE", payload); 1569 } 1570 else if (match_cmd("source\0", cmd)) { 1571 payload = json_object_new_object(); 1572 json_object_object_add(payload, "file", json_object_new_string(line)); 1573 proto_write(fd, "SOURCE", payload); 1574 } 1575 else if (match_cmd("quit\0q\0", cmd)) { 1576 bool force = !strcmp(trim(line), "-f"); 1577 1578 if (!force && isatty(STDIN_FILENO)) { 1579 char confirm[16]; 1580 1581 /* This wants a plain, cooked-mode, blocking fgets() prompt of 1582 * its own - drop out of lineedit's raw/non-blocking mode for 1583 * it, then re-engage before returning. */ 1584 lineedit_suspend(); 1585 1586 printf("Terminate program? (y/n) > "); 1587 fflush(stdout); 1588 1589 bool confirmed = fgets(confirm, sizeof(confirm), stdin) && 1590 tolower((unsigned char)confirm[0]) == 'y'; 1591 1592 lineedit_resume(); 1593 1594 if (!confirmed) { 1595 *sent = false; 1596 return true; 1597 } 1598 } 1599 1600 proto_write(fd, "QUIT", NULL); 1601 return false; 1602 } 1603 else { 1604 printf("Unrecognized command '%s' (try 'help')\n", cmd); 1605 *sent = false; 1606 } 1607 1608 return true; 1609 } 1610 1611 /* -- connection setup ----------------------------------------------------- */ 1612 1613 static int 1614 connect_socket(const char *path) 1615 { 1616 struct sockaddr_un addr; 1617 int fd; 1618 1619 fd = socket(AF_UNIX, SOCK_STREAM, 0); 1620 1621 if (fd < 0) 1622 return -1; 1623 1624 memset(&addr, 0, sizeof(addr)); 1625 addr.sun_family = AF_UNIX; 1626 strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1); 1627 1628 if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { 1629 close(fd); 1630 1631 return -1; 1632 } 1633 1634 return fd; 1635 } 1636 1637 static char * 1638 get_socket_path_for_pid(pid_t pid) 1639 { 1640 static char path[256]; 1641 1642 snprintf(path, sizeof(path), "%s/ucode-debug-%d.sock", DEFAULT_SOCKET_DIR, pid); 1643 1644 return path; 1645 } 1646 1647 static int 1648 wait_for_socket(const char *path, int timeout_sec) 1649 { 1650 int elapsed = 0; 1651 struct stat st; 1652 1653 while (elapsed < timeout_sec) { 1654 if (stat(path, &st) == 0 && (st.st_mode & S_IFMT) == S_IFSOCK) 1655 return 0; 1656 1657 sleep(1); 1658 elapsed++; 1659 } 1660 1661 return -1; 1662 } 1663 1664 /* The debuggee's PID, for Ctrl-C-while-running (see maybe_send_interrupt() 1665 * below) - resolved once right after connecting, however that happened 1666 * (explicit <pid>, a socket path, or an inherited --fd), via SO_PEERCRED: 1667 * works uniformly for all three, since all of them are - or, for --fd, 1668 * were, at the moment the debuggee created it and only then forked - a 1669 * connected AF_UNIX socket. -1 if this somehow couldn't be determined 1670 * (Ctrl-C-while-running is then a no-op; everything else about the 1671 * session is unaffected). */ 1672 static pid_t debuggee_pid = -1; 1673 1674 static void 1675 resolve_debuggee_pid(int fd) 1676 { 1677 #if defined(__linux__) 1678 struct ucred cred; 1679 socklen_t len = sizeof(cred); 1680 1681 if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) == 0) 1682 debuggee_pid = cred.pid; 1683 #elif defined(__APPLE__) 1684 pid_t pid; 1685 socklen_t len = sizeof(pid); 1686 1687 if (getsockopt(fd, SOL_LOCAL, LOCAL_PEERPID, &pid, &len) == 0) 1688 debuggee_pid = pid; 1689 #endif 1690 } 1691 1692 static void 1693 print_usage(const char *prog) 1694 { 1695 fprintf(stderr, "Usage: %s [-s DIR] <pid>\n", prog); 1696 fprintf(stderr, " %s [-s DIR] <socket-path>\n", prog); 1697 fprintf(stderr, " %s [-s DIR] --fd <n>\n", prog); 1698 fprintf(stderr, "\n"); 1699 fprintf(stderr, "Debugger client for ucode, speaking the line-based debug protocol.\n"); 1700 fprintf(stderr, "\n"); 1701 fprintf(stderr, " <pid> SIGUSR1-attach to a running `-X` process, gdb -p style.\n"); 1702 fprintf(stderr, " <socket-path> connect to an explicit debug.listen(path) socket.\n"); 1703 fprintf(stderr, " --fd <n> use an already-connected fd (internal, used by `-x`).\n"); 1704 fprintf(stderr, " -s, --srcdir DIR\n"); 1705 fprintf(stderr, " Local directory to also look for source files under\n"); 1706 fprintf(stderr, " (by basename) when the path the server reports doesn't\n"); 1707 fprintf(stderr, " exist as-is on this machine - e.g. the target runs on a\n"); 1708 fprintf(stderr, " different host/root than this checkout. Source is always\n"); 1709 fprintf(stderr, " tried locally first (at the server's exact reported path)\n"); 1710 fprintf(stderr, " before ever asking the server for it.\n"); 1711 } 1712 1713 int 1714 main(int argc, char **argv) 1715 { 1716 int fd; 1717 fd_set readfds; 1718 char buf[MAX_LINE]; 1719 linebuf_t lb = { 0 }; 1720 1721 signal(SIGPIPE, SIG_IGN); 1722 setvbuf(stdout, NULL, _IOLBF, 0); 1723 debug_highlight_init(); 1724 1725 { 1726 size_t n = sizeof(cli_help_table) / sizeof(cli_help_table[0]); 1727 static lineedit_completion_t comps[sizeof(cli_help_table) / sizeof(cli_help_table[0])]; 1728 1729 for (size_t i = 0; i < n; i++) 1730 comps[i].names = cli_help_table[i].names; 1731 1732 lineedit_set_completions(comps, n); 1733 } 1734 1735 lineedit_init(); 1736 1737 /* Pull -s/--srcdir DIR out of argv wherever it appears, leaving the 1738 * rest of argument parsing below untouched. */ 1739 { 1740 int ai = 1; 1741 1742 while (ai < argc) { 1743 if (!strcmp(argv[ai], "-s") || !strcmp(argv[ai], "--srcdir")) { 1744 if (ai + 1 >= argc) { 1745 print_usage(argv[0]); 1746 1747 return 1; 1748 } 1749 1750 opt_srcdir = argv[ai + 1]; 1751 memmove(&argv[ai], &argv[ai + 2], (size_t)(argc - ai - 2) * sizeof(char *)); 1752 argc -= 2; 1753 1754 continue; 1755 } 1756 1757 ai++; 1758 } 1759 } 1760 1761 if (argc < 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) { 1762 print_usage(argv[0]); 1763 1764 return (argc < 2) ? 1 : 0; 1765 } 1766 1767 if (!strcmp(argv[1], "--fd")) { 1768 if (argc < 3) { 1769 print_usage(argv[0]); 1770 1771 return 1; 1772 } 1773 1774 fd = atoi(argv[2]); 1775 } 1776 else if (strchr(argv[1], '/')) { 1777 fd = connect_socket(argv[1]); 1778 1779 if (fd < 0) { 1780 fprintf(stderr, "Failed to connect to %s: %s\n", argv[1], strerror(errno)); 1781 1782 return 1; 1783 } 1784 } 1785 else { 1786 pid_t pid = atoi(argv[1]); 1787 char *socket_path; 1788 struct stat st; 1789 1790 if (pid <= 0) { 1791 fprintf(stderr, "Invalid PID: %s\n", argv[1]); 1792 1793 return 1; 1794 } 1795 1796 socket_path = get_socket_path_for_pid(pid); 1797 1798 /* If the attach socket already exists, the target already has a 1799 * session waiting (e.g. `-X <expr>`/debug.attach()) - just connect. 1800 * Only send SIGUSR1 for the classic bare `-X` flow, where nothing is 1801 * listening yet until asked to. */ 1802 if (stat(socket_path, &st) == 0 && S_ISSOCK(st.st_mode)) { 1803 fprintf(stderr, "Debugger socket already present, connecting...\n"); 1804 } 1805 else { 1806 if (kill(pid, SIGUSR1) < 0) { 1807 fprintf(stderr, "Failed to send SIGUSR1 to process %d: %s\n", pid, strerror(errno)); 1808 1809 return 1; 1810 } 1811 1812 fprintf(stderr, "Sent SIGUSR1 to process %d, waiting for debugger socket...\n", pid); 1813 1814 if (wait_for_socket(socket_path, MAX_WAIT_TIME) < 0) { 1815 fprintf(stderr, "Timeout waiting for debugger socket at %s\n", socket_path); 1816 1817 return 1; 1818 } 1819 } 1820 1821 fd = connect_socket(socket_path); 1822 1823 if (fd < 0) { 1824 fprintf(stderr, "Failed to connect to %s: %s\n", socket_path, strerror(errno)); 1825 1826 return 1; 1827 } 1828 } 1829 1830 resolve_debuggee_pid(fd); 1831 1832 fprintf(stderr, "Connected to ucode debugger\n\n"); 1833 1834 bool stdin_done = false; 1835 /* True whenever the session is sitting at a PAUSED prompt waiting for 1836 * a command - i.e. exactly when a "dbg > " prompt should be visible. 1837 * Cleared the instant a resuming command (next/step/continue/return) 1838 * is sent, since there is no synchronous ack for those (see 1839 * lib/debug_proto.h) - the prompt only comes back once a new PAUSED 1840 * (or the connection closing) says so. */ 1841 bool paused = false; 1842 /* True from the moment any command is sent until its response has 1843 * actually been drained and rendered - keeps the prompt from 1844 * reappearing (and racing ahead of) a response that just hasn't 1845 * arrived over the socket yet. */ 1846 bool awaiting_response = false; 1847 /* Tracks whether lineedit_begin() has already been called for the 1848 * current accepting_input span, so the prompt (and a fresh, empty 1849 * edit line) is (re)started exactly once per command, not on every 1850 * select() wakeup while still mid-edit. */ 1851 bool prompt_shown = false; 1852 1853 for (;;) { 1854 char *verb; 1855 struct json_object *payload; 1856 1857 while (linebuf_pop(&lb, &verb, &payload)) { 1858 render_response(fd, verb, payload); 1859 awaiting_response = false; 1860 1861 if (!strcmp(verb, "PAUSED")) 1862 paused = true; 1863 else if (!strcmp(verb, "RESUME") || !strcmp(verb, "EVENT")) 1864 paused = false; 1865 1866 free(verb); 1867 json_object_put(payload); 1868 } 1869 1870 /* Only accept (and select on) stdin for actual command input while 1871 * sitting at a prompt: gating this on the exact same condition 1872 * that shows the prompt is what stops a command from racing ahead 1873 * of - and getting interleaved with - the connection's own 1874 * initial PAUSED message or a still-in-flight response to a 1875 * previous command. */ 1876 bool accepting_input = paused && !stdin_done && !awaiting_response 1877 && !pending_source.active && !pending_backtrace.active; 1878 1879 if (!accepting_input) 1880 prompt_shown = false; 1881 else if (!prompt_shown) { 1882 lineedit_begin("dbg > "); 1883 prompt_shown = true; 1884 } 1885 1886 FD_ZERO(&readfds); 1887 1888 /* Outside of accepting_input, stdin is still watched (whenever 1889 * raw-mode editing is active, i.e. a real terminal - piped/ 1890 * scripted input has no Ctrl-C to speak of) purely to catch 1891 * Ctrl-C-while-running: see the interrupt handling below. */ 1892 if (accepting_input || (lineedit_active() && !stdin_done)) 1893 FD_SET(STDIN_FILENO, &readfds); 1894 1895 FD_SET(fd, &readfds); 1896 1897 if (select(fd + 1, &readfds, NULL, NULL, NULL) < 0) { 1898 if (errno == EINTR) 1899 continue; 1900 1901 break; 1902 } 1903 1904 if (FD_ISSET(fd, &readfds)) { 1905 ssize_t n = read(fd, buf, sizeof(buf)); 1906 1907 if (n <= 0) { 1908 printf("\nConnection closed\n"); 1909 break; 1910 } 1911 1912 linebuf_append(&lb, buf, (size_t)n); 1913 } 1914 1915 if (!stdin_done && FD_ISSET(STDIN_FILENO, &readfds) && !accepting_input) { 1916 /* Not sitting at a prompt (the debuggee is running) - stdin is 1917 * only being watched here for Ctrl-C, not full line editing; 1918 * anything else typed while running had no effect before this 1919 * feature existed either, so it's simply discarded rather 1920 * than queued up to confuse the next prompt. lineedit's raw 1921 * mode (a prerequisite for even reaching this branch, see the 1922 * FD_SET above) already made stdin non-blocking. */ 1923 char ibuf[64]; 1924 ssize_t n = read(STDIN_FILENO, ibuf, sizeof(ibuf)); 1925 1926 for (ssize_t i = 0; i < n; i++) { 1927 if (ibuf[i] == 3 /* Ctrl-C */ && debuggee_pid > 0) { 1928 kill(debuggee_pid, SIGUSR1); 1929 break; 1930 } 1931 } 1932 } 1933 else if (!stdin_done && FD_ISSET(STDIN_FILENO, &readfds)) { 1934 bool eof = false; 1935 1936 if (lineedit_feed(buf, sizeof(buf), &eof)) { 1937 prompt_shown = false; 1938 1939 if (*trim(buf)) { 1940 bool resuming, sent; 1941 bool keep_going = send_command(fd, trim(buf), &resuming, &sent); 1942 1943 /* An unrecognized/empty command (or "quit" declined at its 1944 * confirmation prompt) never reaches the server, so there 1945 * is no response to wait for - re-show the prompt right 1946 * away instead of waiting forever for one that isn't 1947 * coming. */ 1948 awaiting_response = sent; 1949 1950 if (resuming) 1951 paused = false; 1952 1953 if (!keep_going) { 1954 /* QUIT was sent - keep looping (without reading 1955 * further stdin) to drain and render any trailing 1956 * responses (e.g. a final EVENT exit) until the 1957 * server closes the connection, instead of exiting 1958 * immediately and losing output that was already in 1959 * flight. */ 1960 stdin_done = true; 1961 } 1962 } 1963 } 1964 else if (eof) { 1965 proto_write(fd, "QUIT", NULL); 1966 stdin_done = true; 1967 } 1968 } 1969 } 1970 1971 close(fd); 1972 1973 return 0; 1974 } 1975
This page was automatically generated by LXR 0.3.1. • OpenWrt