]> git.ipfire.org Git - thirdparty/systemd.git/blob - src/journal/coredumpctl.c
coredump: include stacktrace of coredumps in the log message
[thirdparty/systemd.git] / src / journal / coredumpctl.c
1 /*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
2
3 /***
4 This file is part of systemd.
5
6 Copyright 2012 Zbigniew Jędrzejewski-Szmek
7
8 systemd is free software; you can redistribute it and/or modify it
9 under the terms of the GNU Lesser General Public License as published by
10 the Free Software Foundation; either version 2.1 of the License, or
11 (at your option) any later version.
12
13 systemd is distributed in the hope that it will be useful, but
14 WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 Lesser General Public License for more details.
17
18 You should have received a copy of the GNU Lesser General Public License
19 along with systemd; If not, see <http://www.gnu.org/licenses/>.
20 ***/
21
22 #include <locale.h>
23 #include <stdio.h>
24 #include <string.h>
25 #include <getopt.h>
26 #include <fcntl.h>
27 #include <unistd.h>
28
29 #include <systemd/sd-journal.h>
30
31 #include "build.h"
32 #include "set.h"
33 #include "util.h"
34 #include "log.h"
35 #include "path-util.h"
36 #include "pager.h"
37 #include "macro.h"
38 #include "journal-internal.h"
39 #include "copy.h"
40
41 static enum {
42 ACTION_NONE,
43 ACTION_INFO,
44 ACTION_LIST,
45 ACTION_DUMP,
46 ACTION_GDB,
47 } arg_action = ACTION_LIST;
48
49 static FILE* output = NULL;
50 static const char* arg_field = NULL;
51
52 static int arg_no_pager = false;
53 static int arg_no_legend = false;
54
55 static Set *new_matches(void) {
56 Set *set;
57 char *tmp;
58 int r;
59
60 set = set_new(trivial_hash_func, trivial_compare_func);
61 if (!set) {
62 log_oom();
63 return NULL;
64 }
65
66 tmp = strdup("MESSAGE_ID=fc2e22bc6ee647b6b90729ab34a250b1");
67 if (!tmp) {
68 log_oom();
69 set_free(set);
70 return NULL;
71 }
72
73 r = set_consume(set, tmp);
74 if (r < 0) {
75 log_error("failed to add to set: %s", strerror(-r));
76 set_free(set);
77 return NULL;
78 }
79
80 return set;
81 }
82
83 static int help(void) {
84
85 printf("%s [OPTIONS...]\n\n"
86 "List or retrieve coredumps from the journal.\n\n"
87 "Flags:\n"
88 " -o --output=FILE Write output to FILE\n"
89 " --no-pager Do not pipe output into a pager\n"
90 " --no-legend Do not print the column headers.\n\n"
91
92 "Commands:\n"
93 " -h --help Show this help\n"
94 " --version Print version string\n"
95 " -F --field=FIELD List all values a certain field takes\n"
96 " list [MATCHES...] List available coredumps\n"
97 " info [MATCHES...] Show detailed information about one or more coredumps\n"
98 " dump [MATCHES...] Print first matching coredump to stdout\n"
99 " gdb [MATCHES...] Start gdb for the first matching coredump\n"
100 , program_invocation_short_name);
101
102 return 0;
103 }
104
105 static int add_match(Set *set, const char *match) {
106 int r = -ENOMEM;
107 unsigned pid;
108 const char* prefix;
109 char *pattern = NULL;
110 _cleanup_free_ char *p = NULL;
111
112 if (strchr(match, '='))
113 prefix = "";
114 else if (strchr(match, '/')) {
115 p = path_make_absolute_cwd(match);
116 if (!p)
117 goto fail;
118
119 match = p;
120 prefix = "COREDUMP_EXE=";
121 }
122 else if (safe_atou(match, &pid) == 0)
123 prefix = "COREDUMP_PID=";
124 else
125 prefix = "COREDUMP_COMM=";
126
127 pattern = strjoin(prefix, match, NULL);
128 if (!pattern)
129 goto fail;
130
131 log_debug("Adding pattern: %s", pattern);
132 r = set_put(set, pattern);
133 if (r < 0) {
134 log_error("Failed to add pattern '%s': %s",
135 pattern, strerror(-r));
136 free(pattern);
137 goto fail;
138 }
139
140 return 0;
141 fail:
142 log_error("Failed to add match: %s", strerror(-r));
143 return r;
144 }
145
146 static int parse_argv(int argc, char *argv[], Set *matches) {
147 enum {
148 ARG_VERSION = 0x100,
149 ARG_NO_PAGER,
150 ARG_NO_LEGEND,
151 };
152
153 int r, c;
154
155 static const struct option options[] = {
156 { "help", no_argument, NULL, 'h' },
157 { "version" , no_argument, NULL, ARG_VERSION },
158 { "no-pager", no_argument, NULL, ARG_NO_PAGER },
159 { "no-legend", no_argument, NULL, ARG_NO_LEGEND },
160 { "output", required_argument, NULL, 'o' },
161 { "field", required_argument, NULL, 'F' },
162 {}
163 };
164
165 assert(argc >= 0);
166 assert(argv);
167
168 while ((c = getopt_long(argc, argv, "ho:F:", options, NULL)) >= 0)
169 switch(c) {
170
171 case 'h':
172 arg_action = ACTION_NONE;
173 return help();
174
175 case ARG_VERSION:
176 arg_action = ACTION_NONE;
177 puts(PACKAGE_STRING);
178 puts(SYSTEMD_FEATURES);
179 return 0;
180
181 case ARG_NO_PAGER:
182 arg_no_pager = true;
183 break;
184
185 case ARG_NO_LEGEND:
186 arg_no_legend = true;
187 break;
188
189 case 'o':
190 if (output) {
191 log_error("cannot set output more than once");
192 return -EINVAL;
193 }
194
195 output = fopen(optarg, "we");
196 if (!output) {
197 log_error("writing to '%s': %m", optarg);
198 return -errno;
199 }
200
201 break;
202
203 case 'F':
204 if (arg_field) {
205 log_error("cannot use --field/-F more than once");
206 return -EINVAL;
207 }
208 arg_field = optarg;
209 break;
210
211 case '?':
212 return -EINVAL;
213
214 default:
215 assert_not_reached("Unhandled option");
216 }
217
218 if (optind < argc) {
219 const char *cmd = argv[optind++];
220 if (streq(cmd, "list"))
221 arg_action = ACTION_LIST;
222 else if (streq(cmd, "dump"))
223 arg_action = ACTION_DUMP;
224 else if (streq(cmd, "gdb"))
225 arg_action = ACTION_GDB;
226 else if (streq(cmd, "info"))
227 arg_action = ACTION_INFO;
228 else {
229 log_error("Unknown action '%s'", cmd);
230 return -EINVAL;
231 }
232 }
233
234 if (arg_field && arg_action != ACTION_LIST) {
235 log_error("Option --field/-F only makes sense with list");
236 return -EINVAL;
237 }
238
239 while (optind < argc) {
240 r = add_match(matches, argv[optind]);
241 if (r != 0)
242 return r;
243 optind++;
244 }
245
246 return 0;
247 }
248
249 static int retrieve(const void *data,
250 size_t len,
251 const char *name,
252 char **var) {
253
254 size_t ident;
255 char *v;
256
257 ident = strlen(name) + 1; /* name + "=" */
258
259 if (len < ident)
260 return 0;
261
262 if (memcmp(data, name, ident - 1) != 0)
263 return 0;
264
265 if (((const char*) data)[ident - 1] != '=')
266 return 0;
267
268 v = strndup((const char*)data + ident, len - ident);
269 if (!v)
270 return log_oom();
271
272 free(*var);
273 *var = v;
274
275 return 0;
276 }
277
278 #define filename_escape(s) xescape((s), "./")
279
280 static int make_coredump_path(sd_journal *j, char **ret) {
281 _cleanup_free_ char
282 *pid = NULL, *boot_id = NULL, *tstamp = NULL, *comm = NULL,
283 *p = NULL, *b = NULL, *t = NULL, *c = NULL;
284 const void *d;
285 size_t l;
286 char *fn;
287
288 assert(j);
289 assert(ret);
290
291 SD_JOURNAL_FOREACH_DATA(j, d, l) {
292 retrieve(d, l, "COREDUMP_COMM", &comm);
293 retrieve(d, l, "COREDUMP_PID", &pid);
294 retrieve(d, l, "COREDUMP_TIMESTAMP", &tstamp);
295 retrieve(d, l, "_BOOT_ID", &boot_id);
296 }
297
298 if (!pid || !comm || !tstamp || !boot_id) {
299 log_error("Failed to retrieve necessary fields to find coredump on disk.");
300 return -ENOENT;
301 }
302
303 p = filename_escape(pid);
304 if (!p)
305 return log_oom();
306
307 t = filename_escape(tstamp);
308 if (!t)
309 return log_oom();
310
311 c = filename_escape(comm);
312 if (!t)
313 return log_oom();
314
315 b = filename_escape(boot_id);
316 if (!b)
317 return log_oom();
318
319 fn = strjoin("/var/lib/systemd/coredump/core.", c, ".", b, ".", p, ".", t, NULL);
320 if (!fn)
321 return log_oom();
322
323 *ret = fn;
324 return 0;
325 }
326
327 static void print_field(FILE* file, sd_journal *j) {
328 _cleanup_free_ char *value = NULL;
329 const void *d;
330 size_t l;
331
332 assert(file);
333 assert(j);
334
335 assert(arg_field);
336
337 SD_JOURNAL_FOREACH_DATA(j, d, l)
338 retrieve(d, l, arg_field, &value);
339
340 if (value)
341 fprintf(file, "%s\n", value);
342 }
343
344 static int print_list(FILE* file, sd_journal *j, int had_legend) {
345 _cleanup_free_ char
346 *pid = NULL, *uid = NULL, *gid = NULL,
347 *sgnl = NULL, *exe = NULL, *comm = NULL, *cmdline = NULL;
348 const void *d;
349 size_t l;
350 usec_t t;
351 char buf[FORMAT_TIMESTAMP_MAX];
352 int r;
353
354 assert(file);
355 assert(j);
356
357 SD_JOURNAL_FOREACH_DATA(j, d, l) {
358 retrieve(d, l, "COREDUMP_PID", &pid);
359 retrieve(d, l, "COREDUMP_UID", &uid);
360 retrieve(d, l, "COREDUMP_GID", &gid);
361 retrieve(d, l, "COREDUMP_SIGNAL", &sgnl);
362 retrieve(d, l, "COREDUMP_EXE", &exe);
363 retrieve(d, l, "COREDUMP_COMM", &comm);
364 retrieve(d, l, "COREDUMP_CMDLINE", &cmdline);
365 }
366
367 if (!pid && !uid && !gid && !sgnl && !exe && !comm && !cmdline) {
368 log_warning("Empty coredump log entry");
369 return -EINVAL;
370 }
371
372 r = sd_journal_get_realtime_usec(j, &t);
373 if (r < 0) {
374 log_error("Failed to get realtime timestamp: %s", strerror(-r));
375 return r;
376 }
377
378 format_timestamp(buf, sizeof(buf), t);
379
380 if (!had_legend && !arg_no_legend)
381 fprintf(file, "%-*s %*s %*s %*s %*s %s\n",
382 FORMAT_TIMESTAMP_MAX-1, "TIME",
383 6, "PID",
384 5, "UID",
385 5, "GID",
386 3, "SIG",
387 "EXE");
388
389 fprintf(file, "%*s %*s %*s %*s %*s %s\n",
390 FORMAT_TIMESTAMP_MAX-1, buf,
391 6, strna(pid),
392 5, strna(uid),
393 5, strna(gid),
394 3, strna(sgnl),
395 strna(exe ?: (comm ?: cmdline)));
396
397 return 0;
398 }
399
400 static int print_info(FILE *file, sd_journal *j, bool need_space) {
401 _cleanup_free_ char
402 *pid = NULL, *uid = NULL, *gid = NULL,
403 *sgnl = NULL, *exe = NULL, *comm = NULL, *cmdline = NULL,
404 *unit = NULL, *user_unit = NULL, *session = NULL,
405 *boot_id = NULL, *machine_id = NULL, *hostname = NULL,
406 *coredump = NULL, *slice = NULL, *cgroup = NULL,
407 *owner_uid = NULL, *message = NULL;
408 const void *d;
409 size_t l;
410
411 assert(file);
412 assert(j);
413
414 SD_JOURNAL_FOREACH_DATA(j, d, l) {
415 retrieve(d, l, "COREDUMP_PID", &pid);
416 retrieve(d, l, "COREDUMP_UID", &uid);
417 retrieve(d, l, "COREDUMP_GID", &gid);
418 retrieve(d, l, "COREDUMP_SIGNAL", &sgnl);
419 retrieve(d, l, "COREDUMP_EXE", &exe);
420 retrieve(d, l, "COREDUMP_COMM", &comm);
421 retrieve(d, l, "COREDUMP_CMDLINE", &cmdline);
422 retrieve(d, l, "COREDUMP_UNIT", &unit);
423 retrieve(d, l, "COREDUMP_USER_UNIT", &user_unit);
424 retrieve(d, l, "COREDUMP_SESSION", &session);
425 retrieve(d, l, "COREDUMP_OWNER_UID", &owner_uid);
426 retrieve(d, l, "COREDUMP_SLICE", &slice);
427 retrieve(d, l, "COREDUMP_CGROUP", &cgroup);
428 retrieve(d, l, "_BOOT_ID", &boot_id);
429 retrieve(d, l, "_MACHINE_ID", &machine_id);
430 retrieve(d, l, "_HOSTNAME", &hostname);
431 retrieve(d, l, "MESSAGE", &message);
432 }
433
434 if (need_space)
435 fputs("\n", file);
436
437 fprintf(file,
438 " PID: %s%s%s\n",
439 ansi_highlight(), strna(pid), ansi_highlight_off());
440
441 if (uid) {
442 uid_t n;
443
444 if (parse_uid(uid, &n) >= 0) {
445 _cleanup_free_ char *u = NULL;
446
447 u = uid_to_name(n);
448 fprintf(file,
449 " UID: %s (%s)\n",
450 uid, u);
451 } else {
452 fprintf(file,
453 " UID: %s\n",
454 uid);
455 }
456 }
457
458 if (gid) {
459 gid_t n;
460
461 if (parse_gid(gid, &n) >= 0) {
462 _cleanup_free_ char *g = NULL;
463
464 g = gid_to_name(n);
465 fprintf(file,
466 " GID: %s (%s)\n",
467 gid, g);
468 } else {
469 fprintf(file,
470 " GID: %s\n",
471 gid);
472 }
473 }
474
475 if (sgnl) {
476 int sig;
477
478 if (safe_atoi(sgnl, &sig) >= 0)
479 fprintf(file, " Signal: %s (%s)\n", sgnl, signal_to_string(sig));
480 else
481 fprintf(file, " Signal: %s\n", sgnl);
482 }
483
484 if (exe)
485 fprintf(file, " Executable: %s%s%s\n", ansi_highlight(), exe, ansi_highlight_off());
486 if (comm)
487 fprintf(file, " Comm: %s\n", comm);
488 if (cmdline)
489 fprintf(file, " Command Line: %s\n", cmdline);
490 if (cgroup)
491 fprintf(file, " Control Group: %s\n", cgroup);
492 if (unit)
493 fprintf(file, " Unit: %s\n", unit);
494 if (user_unit)
495 fprintf(file, " User Unit: %s\n", unit);
496 if (slice)
497 fprintf(file, " Slice: %s\n", slice);
498 if (session)
499 fprintf(file, " Session: %s\n", session);
500 if (owner_uid) {
501 uid_t n;
502
503 if (parse_uid(owner_uid, &n) >= 0) {
504 _cleanup_free_ char *u = NULL;
505
506 u = uid_to_name(n);
507 fprintf(file,
508 " Owner UID: %s (%s)\n",
509 owner_uid, u);
510 } else {
511 fprintf(file,
512 " Owner UID: %s\n",
513 owner_uid);
514 }
515 }
516 if (boot_id)
517 fprintf(file, " Boot ID: %s\n", boot_id);
518 if (machine_id)
519 fprintf(file, " Machine ID: %s\n", machine_id);
520 if (hostname)
521 fprintf(file, " Hostname: %s\n", hostname);
522
523 if (make_coredump_path(j, &coredump) >= 0)
524 if (access(coredump, F_OK) >= 0)
525 fprintf(file, " Coredump: %s\n", coredump);
526
527 if (message) {
528 _cleanup_free_ char *m = NULL;
529
530 m = strreplace(message, "\n", "\n ");
531
532 fprintf(file, " Message: %s\n", strstrip(m ?: message));
533 }
534
535 return 0;
536 }
537
538 static int dump_list(sd_journal *j) {
539 int found = 0;
540
541 assert(j);
542
543 /* The coredumps are likely to compressed, and for just
544 * listing them we don't need to decompress them, so let's
545 * pick a fairly low data threshold here */
546 sd_journal_set_data_threshold(j, 4096);
547
548 SD_JOURNAL_FOREACH(j) {
549 if (arg_action == ACTION_INFO)
550 print_info(stdout, j, found++);
551 else if (arg_field)
552 print_field(stdout, j);
553 else
554 print_list(stdout, j, found++);
555 }
556
557 if (!arg_field && !found) {
558 log_notice("No coredumps found");
559 return -ESRCH;
560 }
561
562 return 0;
563 }
564
565 static int focus(sd_journal *j) {
566 int r;
567
568 r = sd_journal_seek_tail(j);
569 if (r == 0)
570 r = sd_journal_previous(j);
571 if (r < 0) {
572 log_error("Failed to search journal: %s", strerror(-r));
573 return r;
574 }
575 if (r == 0) {
576 log_error("No match found");
577 return -ESRCH;
578 }
579 return r;
580 }
581
582 static int dump_core(sd_journal* j) {
583 const void *data;
584 size_t len, ret;
585 int r;
586
587 assert(j);
588
589 /* We want full data, nothing truncated. */
590 sd_journal_set_data_threshold(j, 0);
591
592 r = focus(j);
593 if (r < 0)
594 return r;
595
596 print_info(output ? stdout : stderr, j, false);
597
598 if (on_tty() && !output) {
599 log_error("Refusing to dump core to tty.");
600 return -ENOTTY;
601 }
602
603 r = sd_journal_get_data(j, "COREDUMP", (const void**) &data, &len);
604 if (r == ENOENT) {
605 _cleanup_free_ char *fn = NULL;
606 _cleanup_close_ int fd = -1;
607
608 r = make_coredump_path(j, &fn);
609 if (r < 0)
610 return r;
611
612 fd = open(fn, O_RDONLY|O_CLOEXEC|O_NOCTTY);
613 if (fd < 0) {
614 if (errno == ENOENT)
615 log_error("Coredump neither in journal file nor stored externally on disk.");
616 else
617 log_error("Failed to open coredump file: %s", strerror(-r));
618
619 return -errno;
620 }
621
622 r = copy_bytes(fd, output ? fileno(output) : STDOUT_FILENO);
623 if (r < 0) {
624 log_error("Failed to stream coredump: %s", strerror(-r));
625 return r;
626 }
627
628 } else if (r < 0) {
629 log_error("Failed to retrieve COREDUMP field: %s", strerror(-r));
630 return r;
631
632 } else {
633 assert(len >= 9);
634 data = (const uint8_t*) data + 9;
635 len -= 9;
636
637 ret = fwrite(data, len, 1, output ?: stdout);
638 if (ret != 1) {
639 log_error("Dumping coredump failed: %m (%zu)", ret);
640 return -errno;
641 }
642 }
643
644 r = sd_journal_previous(j);
645 if (r >= 0)
646 log_warning("More than one entry matches, ignoring rest.");
647
648 return 0;
649 }
650
651 static int run_gdb(sd_journal *j) {
652
653 _cleanup_free_ char *exe = NULL, *coredump = NULL;
654 char temp[] = "/var/tmp/coredump-XXXXXX";
655 bool unlink_temp = false;
656 const char *path;
657 const void *data;
658 siginfo_t st;
659 size_t len;
660 pid_t pid;
661 int r;
662
663 assert(j);
664
665 sd_journal_set_data_threshold(j, 0);
666
667 r = focus(j);
668 if (r < 0)
669 return r;
670
671 print_info(stdout, j, false);
672 fputs("\n", stdout);
673
674 r = sd_journal_get_data(j, "COREDUMP_EXE", (const void**) &data, &len);
675 if (r < 0) {
676 log_error("Failed to retrieve COREDUMP_EXE field: %s", strerror(-r));
677 return r;
678 }
679
680 assert(len >= 13);
681 data = (const uint8_t*) data + 13;
682 len -= 13;
683
684 exe = strndup(data, len);
685 if (!exe)
686 return log_oom();
687
688 if (endswith(exe, " (deleted)")) {
689 log_error("Binary already deleted.");
690 return -ENOENT;
691 }
692
693 if (!path_is_absolute(exe)) {
694 log_error("Binary is not an absolute path.");
695 return -ENOENT;
696 }
697
698 r = sd_journal_get_data(j, "COREDUMP", (const void**) &data, &len);
699 if (r == -ENOENT) {
700
701 r = make_coredump_path(j, &coredump);
702 if (r < 0)
703 return r;
704
705 if (access(coredump, R_OK) < 0) {
706 if (errno == ENOENT)
707 log_error("Coredump neither in journal file nor stored externally on disk.");
708 else
709 log_error("Failed to access coredump file: %m");
710
711 return -errno;
712 }
713
714 path = coredump;
715
716 } else if (r < 0) {
717 log_error("Failed to retrieve COREDUMP field: %s", strerror(-r));
718 return r;
719
720 } else {
721 _cleanup_close_ int fd = -1;
722 ssize_t sz;
723
724 assert(len >= 9);
725 data = (const uint8_t*) data + 9;
726 len -= 9;
727
728 fd = mkostemp_safe(temp, O_WRONLY|O_CLOEXEC);
729 if (fd < 0) {
730 log_error("Failed to create temporary file: %m");
731 return -errno;
732 }
733
734 unlink_temp = true;
735
736 sz = write(fd, data, len);
737 if (sz < 0) {
738 log_error("Failed to write temporary file: %m");
739 r = -errno;
740 goto finish;
741 }
742 if (sz != (ssize_t) len) {
743 log_error("Short write to temporary file.");
744 r = -EIO;
745 goto finish;
746 }
747
748 path = temp;
749 }
750
751 pid = fork();
752 if (pid < 0) {
753 log_error("Failed to fork(): %m");
754 r = -errno;
755 goto finish;
756 }
757 if (pid == 0) {
758 execlp("gdb", "gdb", exe, path, NULL);
759
760 log_error("Failed to invoke gdb: %m");
761 _exit(1);
762 }
763
764 r = wait_for_terminate(pid, &st);
765 if (r < 0) {
766 log_error("Failed to wait for gdb: %m");
767 goto finish;
768 }
769
770 r = st.si_code == CLD_EXITED ? st.si_status : 255;
771
772 finish:
773 if (unlink_temp)
774 unlink(temp);
775
776 return r;
777 }
778
779 int main(int argc, char *argv[]) {
780 _cleanup_journal_close_ sd_journal*j = NULL;
781 const char* match;
782 Iterator it;
783 int r = 0;
784 _cleanup_set_free_free_ Set *matches = NULL;
785
786 setlocale(LC_ALL, "");
787 log_parse_environment();
788 log_open();
789
790 matches = new_matches();
791 if (!matches) {
792 r = -ENOMEM;
793 goto end;
794 }
795
796 r = parse_argv(argc, argv, matches);
797 if (r < 0)
798 goto end;
799
800 if (arg_action == ACTION_NONE)
801 goto end;
802
803 r = sd_journal_open(&j, SD_JOURNAL_LOCAL_ONLY);
804 if (r < 0) {
805 log_error("Failed to open journal: %s", strerror(-r));
806 goto end;
807 }
808
809 SET_FOREACH(match, matches, it) {
810 r = sd_journal_add_match(j, match, strlen(match));
811 if (r != 0) {
812 log_error("Failed to add match '%s': %s",
813 match, strerror(-r));
814 goto end;
815 }
816 }
817
818 if (_unlikely_(log_get_max_level() >= LOG_PRI(LOG_DEBUG))) {
819 _cleanup_free_ char *filter;
820
821 filter = journal_make_match_string(j);
822 log_debug("Journal filter: %s", filter);
823 }
824
825 switch(arg_action) {
826
827 case ACTION_LIST:
828 case ACTION_INFO:
829 if (!arg_no_pager)
830 pager_open(false);
831
832 r = dump_list(j);
833 break;
834
835 case ACTION_DUMP:
836 r = dump_core(j);
837 break;
838
839 case ACTION_GDB:
840 r = run_gdb(j);
841 break;
842
843 default:
844 assert_not_reached("Shouldn't be here");
845 }
846
847 end:
848 pager_close();
849
850 if (output)
851 fclose(output);
852
853 return r >= 0 ? r : EXIT_FAILURE;
854 }