• source navigation  • diff markup  • identifier search  • freetext search  • 

Sources/ucode/udbg.c

  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