]> git.ipfire.org Git - thirdparty/systemd.git/blobdiff - src/journal/journalctl.c
Merge pull request #30284 from YHNdnzj/fstab-wantedby-defaultdeps
[thirdparty/systemd.git] / src / journal / journalctl.c
index 3424819540aaede7d57916d4d16ff9c61e6369ec..fe68ec4310b3dc40358e30d3228bca5d4e69c588 100644 (file)
@@ -95,6 +95,7 @@ static bool arg_full = true;
 static bool arg_all = false;
 static PagerFlags arg_pager_flags = 0;
 static int arg_lines = ARG_LINES_DEFAULT;
+static bool arg_lines_oldest = false;
 static bool arg_no_tail = false;
 static bool arg_truncate_newline = false;
 static bool arg_quiet = false;
@@ -174,12 +175,6 @@ static enum {
         ACTION_LIST_FIELD_NAMES,
 } arg_action = ACTION_SHOW;
 
-typedef struct BootId {
-        sd_id128_t id;
-        usec_t first_usec;
-        usec_t last_usec;
-} BootId;
-
 static int add_matches_for_device(sd_journal *j, const char *devpath) {
         _cleanup_(sd_device_unrefp) sd_device *device = NULL;
         sd_device *d = NULL;
@@ -298,6 +293,45 @@ static int parse_boot_descriptor(const char *x, sd_id128_t *boot_id, int *offset
         return 1;
 }
 
+static int parse_lines(const char *arg, bool graceful) {
+        const char *l;
+        int n, r;
+
+        assert(arg || graceful);
+
+        if (!arg)
+                goto default_noarg;
+
+        if (streq(arg, "all")) {
+                arg_lines = ARG_LINES_ALL;
+                return 1;
+        }
+
+        l = startswith(arg, "+");
+
+        r = safe_atoi(l ?: arg, &n);
+        if (r < 0 || n < 0) {
+                if (graceful)
+                        goto default_noarg;
+
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse --lines='%s'.", arg);
+        }
+
+        arg_lines = n;
+        arg_lines_oldest = l;
+
+        return 1;
+
+default_noarg:
+        arg_lines = 10;
+        arg_lines_oldest = false;
+        return 0;
+}
+
+static bool arg_lines_needs_seek_end(void) {
+        return arg_lines >= 0 && !arg_lines_oldest;
+}
+
 static int help_facilities(void) {
         if (!arg_quiet)
                 puts("Available facilities:");
@@ -331,7 +365,7 @@ static int help(void) {
                "  -M --machine=CONTAINER     Operate on local container\n"
                "  -m --merge                 Show entries from all available journals\n"
                "  -D --directory=PATH        Show journal files from directory\n"
-               "     --file=PATH             Show journal file\n"
+               "  -i --file=PATH             Show journal file\n"
                "     --root=PATH             Operate on an alternate filesystem root\n"
                "     --image=PATH            Operate on disk image as filesystem root\n"
                "     --image-policy=POLICY   Specify disk image dissection policy\n"
@@ -358,7 +392,7 @@ static int help(void) {
                "                               json, json-pretty, json-sse, json-seq, cat,\n"
                "                               with-unit)\n"
                "     --output-fields=LIST    Select fields to print in verbose/export/json modes\n"
-               "  -n --lines[=INTEGER]       Number of journal entries to show\n"
+               "  -n --lines[=[+]INTEGER]    Number of journal entries to show\n"
                "  -r --reverse               Show the newest entries first\n"
                "     --show-cursor           Print the cursor after all the entries\n"
                "     --utc                   Express time in Coordinated Universal Time (UTC)\n"
@@ -427,7 +461,6 @@ static int parse_argv(int argc, char *argv[]) {
                 ARG_HEADER,
                 ARG_FACILITY,
                 ARG_SETUP_KEYS,
-                ARG_FILE,
                 ARG_INTERVAL,
                 ARG_VERIFY,
                 ARG_VERIFY_KEY,
@@ -480,7 +513,7 @@ static int parse_argv(int argc, char *argv[]) {
                 { "system",               no_argument,       NULL, ARG_SYSTEM               },
                 { "user",                 no_argument,       NULL, ARG_USER                 },
                 { "directory",            required_argument, NULL, 'D'                      },
-                { "file",                 required_argument, NULL, ARG_FILE                 },
+                { "file",                 required_argument, NULL, 'i'                      },
                 { "root",                 required_argument, NULL, ARG_ROOT                 },
                 { "image",                required_argument, NULL, ARG_IMAGE                },
                 { "image-policy",         required_argument, NULL, ARG_IMAGE_POLICY         },
@@ -531,7 +564,7 @@ static int parse_argv(int argc, char *argv[]) {
         assert(argc >= 0);
         assert(argv);
 
-        while ((c = getopt_long(argc, argv, "hefo:aln::qmb::kD:p:g:c:S:U:t:u:NF:xrM:", options, NULL)) >= 0)
+        while ((c = getopt_long(argc, argv, "hefo:aln::qmb::kD:p:g:c:S:U:t:u:NF:xrM:i:", options, NULL)) >= 0)
 
                 switch (c) {
 
@@ -592,33 +625,11 @@ static int parse_argv(int argc, char *argv[]) {
                         break;
 
                 case 'n':
-                        if (optarg) {
-                                if (streq(optarg, "all"))
-                                        arg_lines = ARG_LINES_ALL;
-                                else {
-                                        r = safe_atoi(optarg, &arg_lines);
-                                        if (r < 0 || arg_lines < 0)
-                                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse lines '%s'", optarg);
-                                }
-                        } else {
-                                arg_lines = 10;
-
-                                /* Hmm, no argument? Maybe the next
-                                 * word on the command line is
-                                 * supposed to be the argument? Let's
-                                 * see if there is one, and is
-                                 * parsable. */
-                                if (optind < argc) {
-                                        int n;
-                                        if (streq(argv[optind], "all")) {
-                                                arg_lines = ARG_LINES_ALL;
-                                                optind++;
-                                        } else if (safe_atoi(argv[optind], &n) >= 0 && n >= 0) {
-                                                arg_lines = n;
-                                                optind++;
-                                        }
-                                }
-                        }
+                        r = parse_lines(optarg ?: argv[optind], !optarg);
+                        if (r < 0)
+                                return r;
+                        if (r > 0 && !optarg)
+                                optind++;
 
                         break;
 
@@ -715,11 +726,11 @@ static int parse_argv(int argc, char *argv[]) {
                         arg_directory = optarg;
                         break;
 
-                case ARG_FILE:
+                case 'i':
                         if (streq(optarg, "-"))
                                 /* An undocumented feature: we can read journal files from STDIN. We don't document
                                  * this though, since after all we only support this for mmap-able, seekable files, and
-                                 * not for example pipes which are probably the primary usecase for reading things from
+                                 * not for example pipes which are probably the primary use case for reading things from
                                  * STDIN. To avoid confusion we hence don't document this feature. */
                                 arg_file_stdin = true;
                         else {
@@ -1077,13 +1088,17 @@ static int parse_argv(int argc, char *argv[]) {
                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
                                        "--since= must be before --until=.");
 
-        if (!!arg_cursor + !!arg_after_cursor + !!arg_since_set > 1)
+        if (!!arg_cursor + !!arg_after_cursor + !!arg_cursor_file + !!arg_since_set > 1)
                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
-                                       "Please specify only one of --since=, --cursor=, and --after-cursor=.");
+                                       "Please specify only one of --since=, --cursor=, --cursor-file=, and --after-cursor=.");
 
         if (arg_follow && arg_reverse)
                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
-                                       "Please specify either --reverse= or --follow=, not both.");
+                                       "Please specify either --reverse or --follow, not both.");
+
+        if (arg_lines >= 0 && arg_lines_oldest && (arg_reverse || arg_follow))
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                       "--lines=+N is unsupported when --reverse or --follow is specified.");
 
         if (!IN_SET(arg_action, ACTION_SHOW, ACTION_DUMP_CATALOG, ACTION_LIST_CATALOG) && optind < argc)
                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
@@ -1110,11 +1125,12 @@ static int parse_argv(int argc, char *argv[]) {
                 if (r < 0)
                         return r;
 
-                /* When --grep is used along with --lines, we don't know how many lines we can print.
-                 * So we search backwards and count until enough lines have been printed or we hit the head.
+                /* When --grep is used along with --lines without '+', i.e. when we start from the end of the
+                 * journal, we don't know how many lines we can print. So we search backwards and count until
+                 * enough lines have been printed or we hit the head.
                  * An exception is that --follow might set arg_lines, so let's not imply --reverse
                  * if that is specified. */
-                if (arg_lines >= 0 && !arg_follow)
+                if (arg_lines_needs_seek_end() && !arg_follow)
                         arg_reverse = true;
         }
 
@@ -1202,207 +1218,6 @@ static int add_matches(sd_journal *j, char **args) {
         return 0;
 }
 
-static int discover_next_boot(
-                sd_journal *j,
-                sd_id128_t previous_boot_id,
-                bool advance_older,
-                BootId *ret) {
-
-        BootId boot;
-        int r;
-
-        assert(j);
-        assert(ret);
-
-        /* We expect the journal to be on the last position of a boot
-         * (in relation to the direction we are going), so that the next
-         * invocation of sd_journal_next/previous will be from a different
-         * boot. We then collect any information we desire and then jump
-         * to the last location of the new boot by using a _BOOT_ID match
-         * coming from the other journal direction. */
-
-        /* Make sure we aren't restricted by any _BOOT_ID matches, so that
-         * we can actually advance to a *different* boot. */
-        sd_journal_flush_matches(j);
-
-        do {
-                if (advance_older)
-                        r = sd_journal_previous(j);
-                else
-                        r = sd_journal_next(j);
-                if (r < 0)
-                        return r;
-                else if (r == 0) {
-                        *ret = (BootId) {};
-                        return 0; /* End of journal, yay. */
-                }
-
-                r = sd_journal_get_monotonic_usec(j, NULL, &boot.id);
-                if (r < 0)
-                        return r;
-
-                /* We iterate through this in a loop, until the boot ID differs from the previous one. Note that
-                 * normally, this will only require a single iteration, as we moved to the last entry of the previous
-                 * boot entry already. However, it might happen that the per-journal-field entry arrays are less
-                 * complete than the main entry array, and hence might reference an entry that's not actually the last
-                 * one of the boot ID as last one. Let's hence use the per-field array is initial seek position to
-                 * speed things up, but let's not trust that it is complete, and hence, manually advance as
-                 * necessary. */
-
-        } while (sd_id128_equal(boot.id, previous_boot_id));
-
-        r = sd_journal_get_realtime_usec(j, &boot.first_usec);
-        if (r < 0)
-                return r;
-
-        /* Now seek to the last occurrence of this boot ID. */
-        r = add_match_boot_id(j, boot.id);
-        if (r < 0)
-                return r;
-
-        if (advance_older)
-                r = sd_journal_seek_head(j);
-        else
-                r = sd_journal_seek_tail(j);
-        if (r < 0)
-                return r;
-
-        if (advance_older)
-                r = sd_journal_next(j);
-        else
-                r = sd_journal_previous(j);
-        if (r < 0)
-                return r;
-        else if (r == 0)
-                return log_debug_errno(SYNTHETIC_ERRNO(ENODATA),
-                                       "Whoopsie! We found a boot ID but can't read its last entry."); /* This shouldn't happen. We just came from this very boot ID. */
-
-        r = sd_journal_get_realtime_usec(j, &boot.last_usec);
-        if (r < 0)
-                return r;
-
-        sd_journal_flush_matches(j);
-        *ret = boot;
-        return 1;
-}
-
-static int find_boot_by_id(sd_journal *j) {
-        int r;
-
-        assert(j);
-
-        sd_journal_flush_matches(j);
-
-        r = add_match_boot_id(j, arg_boot_id);
-        if (r < 0)
-                return r;
-
-        r = sd_journal_seek_head(j); /* seek to oldest */
-        if (r < 0)
-                return r;
-
-        r = sd_journal_next(j);      /* read the oldest entry */
-        if (r < 0)
-                return r;
-
-        /* At this point the read pointer is positioned at the oldest occurrence of the reference boot ID.
-         * After flushing the matches, one more invocation of _previous() will hence place us at the
-         * following entry, which must then have an older boot ID */
-
-        sd_journal_flush_matches(j);
-        return r > 0;
-}
-
-static int find_boot_by_offset(sd_journal *j) {
-        bool advance_older, skip_once;
-        int r;
-
-        /* Adjust for the asymmetry that offset 0 is the last (and current) boot, while 1 is considered the
-         * (chronological) first boot in the journal. */
-        advance_older = skip_once = arg_boot_offset <= 0;
-
-        if (advance_older)
-                r = sd_journal_seek_tail(j); /* seek to newest */
-        else
-                r = sd_journal_seek_head(j); /* seek to oldest */
-        if (r < 0)
-                return r;
-
-        /* No sd_journal_next()/_previous() here.
-         *
-         * At this point the read pointer is positioned after the newest/before the oldest entry in the whole
-         * journal. The next invocation of _previous()/_next() will hence position us at the newest/oldest
-         * entry we have. */
-
-        int offset = arg_boot_offset;
-        sd_id128_t previous_boot_id = SD_ID128_NULL;
-        for (;;) {
-                BootId boot;
-
-                r = discover_next_boot(j, previous_boot_id, advance_older, &boot);
-                if (r <= 0)
-                        return r;
-
-                previous_boot_id = boot.id;
-
-                if (!skip_once)
-                        offset += advance_older ? 1 : -1;
-                skip_once = false;
-
-                if (offset == 0) {
-                        arg_boot_id = boot.id;
-                        return true;
-                }
-        }
-}
-
-static int get_boots(sd_journal *j, BootId **ret_boots, size_t *ret_n_boots) {
-        _cleanup_free_ BootId *boots = NULL;
-        size_t n_boots = 0;
-        int r;
-
-        assert(j);
-        assert(ret_boots);
-        assert(ret_n_boots);
-
-        r = sd_journal_seek_head(j); /* seek to oldest */
-        if (r < 0)
-                return r;
-
-        /* No sd_journal_next()/_previous() here.
-         *
-         * At this point the read pointer is positioned before the oldest entry in the whole journal. The
-         * next invocation of _next() will hence position us at the oldest entry we have. */
-
-        sd_id128_t previous_boot_id = SD_ID128_NULL;
-        for (;;) {
-                BootId boot;
-
-                r = discover_next_boot(j, previous_boot_id, /* advance_older = */ false, &boot);
-                if (r < 0)
-                        return r;
-                if (r == 0)
-                        break;
-
-                previous_boot_id = boot.id;
-
-                FOREACH_ARRAY(i, boots, n_boots)
-                        if (sd_id128_equal(i->id, boot.id))
-                                /* The boot id is already stored, something wrong with the journal files.
-                                 * Exiting as otherwise this problem would cause an infinite loop. */
-                                break;
-
-                if (!GREEDY_REALLOC(boots, n_boots + 1))
-                        return -ENOMEM;
-
-                boots[n_boots++] = boot;
-        }
-
-        *ret_boots = TAKE_PTR(boots);
-        *ret_n_boots = n_boots;
-        return n_boots > 0;
-}
-
 static int list_boots(sd_journal *j) {
         _cleanup_(table_unrefp) Table *table = NULL;
         _cleanup_free_ BootId *boots = NULL;
@@ -1411,7 +1226,7 @@ static int list_boots(sd_journal *j) {
 
         assert(j);
 
-        r = get_boots(j, &boots, &n_boots);
+        r = journal_get_boots(j, &boots, &n_boots);
         if (r < 0)
                 return log_error_errno(r, "Failed to determine boots: %m");
         if (r == 0)
@@ -1465,7 +1280,7 @@ static int add_boot(sd_journal *j) {
                 return add_match_this_boot(j, arg_machine);
 
         if (sd_id128_is_null(arg_boot_id)) {
-                r = find_boot_by_offset(j);
+                r = journal_find_boot_by_offset(j, arg_boot_offset, &arg_boot_id);
                 if (r < 0)
                         return log_error_errno(r, "Failed to find journal entry from the specified boot offset (%+i): %m",
                                                arg_boot_offset);
@@ -1474,7 +1289,7 @@ static int add_boot(sd_journal *j) {
                                                "No journal boot entry found from the specified boot offset (%+i).",
                                                arg_boot_offset);
         } else {
-                r = find_boot_by_id(j);
+                r = journal_find_boot_by_id(j, arg_boot_id);
                 if (r < 0)
                         return log_error_errno(r, "Failed to find journal entry from the specified boot ID (%s): %m",
                                                SD_ID128_TO_STRING(arg_boot_id));
@@ -1891,11 +1706,11 @@ static int setup_keys(void) {
                 .fsprg_state_size = htole64(state_size),
         };
 
-        r = loop_write(fd, &h, sizeof(h), false);
+        r = loop_write(fd, &h, sizeof(h));
         if (r < 0)
                 return log_error_errno(r, "Failed to write header: %m");
 
-        r = loop_write(fd, state, state_size, false);
+        r = loop_write(fd, state, state_size);
         if (r < 0)
                 return log_error_errno(r, "Failed to write state: %m");
 
@@ -2310,7 +2125,6 @@ static int on_signal(sd_event_source *s, const struct signalfd_siginfo *si, void
 
 static int setup_event(Context *c, int fd, sd_event **ret) {
         _cleanup_(sd_event_unrefp) sd_event *e = NULL;
-        struct stat st;
         int r;
 
         assert(arg_follow);
@@ -2329,15 +2143,15 @@ static int setup_event(Context *c, int fd, sd_event **ret) {
         if (r < 0)
                 return log_error_errno(r, "Failed to add io event source for journal: %m");
 
-        if (fstat(STDOUT_FILENO, &st) < 0)
-                return log_error_errno(r, "Failed to stat stdout: %m");
-
-        if (IN_SET(st.st_mode & S_IFMT, S_IFCHR, S_IFIFO, S_IFSOCK)) {
-                /* Also keeps an eye on STDOUT, and exits as soon as we see a POLLHUP on that, i.e. when it is closed. */
-                r = sd_event_add_io(e, NULL, STDOUT_FILENO, EPOLLHUP|EPOLLERR, NULL, INT_TO_PTR(-ECANCELED));
-                if (r < 0)
-                        return log_error_errno(r, "Failed to add io event source for stdout: %m");
-        }
+        /* Also keeps an eye on STDOUT, and exits as soon as we see a POLLHUP on that, i.e. when it is closed. */
+        r = sd_event_add_io(e, NULL, STDOUT_FILENO, EPOLLHUP|EPOLLERR, NULL, INT_TO_PTR(-ECANCELED));
+        if (r == -EPERM)
+                /* Installing an epoll watch on a regular file doesn't work and fails with EPERM. Which is
+                 * totally OK, handle it gracefully. epoll_ctl() documents EPERM as the error returned when
+                 * the specified fd doesn't support epoll, hence it's safe to check for that. */
+                log_debug_errno(r, "Unable to install EPOLLHUP watch on stderr, not watching for hangups.");
+        else if (r < 0)
+                return log_error_errno(r, "Failed to add io event source for stdout: %m");
 
         if (arg_lines != 0 || arg_since_set) {
                 r = sd_event_add_defer(e, NULL, on_first_event, c);
@@ -2350,10 +2164,12 @@ static int setup_event(Context *c, int fd, sd_event **ret) {
 }
 
 static int run(int argc, char *argv[]) {
-        bool need_seek = false, since_seeked = false, use_cursor = false, after_cursor = false;
+        bool need_seek = false, since_seeked = false, after_cursor = false;
         _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
         _cleanup_(umount_and_freep) char *mounted_dir = NULL;
         _cleanup_(sd_journal_closep) sd_journal *j = NULL;
+        _cleanup_free_ char *cursor_from_file = NULL;
+        const char *cursor = NULL;
         int n_shown, r, poll_fd = -EBADF;
 
         setlocale(LC_ALL, "");
@@ -2630,8 +2446,7 @@ static int run(int argc, char *argv[]) {
         }
 
         if (arg_cursor || arg_after_cursor || arg_cursor_file) {
-                _cleanup_free_ char *cursor_from_file = NULL;
-                const char *cursor = arg_cursor ?: arg_after_cursor;
+                cursor = arg_cursor ?: arg_after_cursor;
 
                 if (arg_cursor_file) {
                         r = read_one_line_file(arg_cursor_file, &cursor_from_file);
@@ -2644,32 +2459,42 @@ static int run(int argc, char *argv[]) {
                         }
                 } else
                         after_cursor = arg_after_cursor;
+        }
 
-                if (cursor) {
-                        r = sd_journal_seek_cursor(j, cursor);
-                        if (r < 0)
-                                return log_error_errno(r, "Failed to seek to cursor: %m");
+        if (cursor) {
+                r = sd_journal_seek_cursor(j, cursor);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to seek to cursor: %m");
 
-                        use_cursor = true;
+                r = sd_journal_step_one(j, !arg_reverse);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to iterate through journal: %m");
+
+                if (after_cursor && r > 0) {
+                        /* With --after-cursor=/--cursor-file= we want to skip the first entry only if it's
+                         * the entry the cursor is pointing at, otherwise, if some journal filters are used,
+                         * we might skip the first entry of the filter match, which leads to unexpectedly
+                         * missing journal entries. */
+                        int k;
+
+                        k = sd_journal_test_cursor(j, cursor);
+                        if (k < 0)
+                                return log_error_errno(k, "Failed to test cursor against current entry: %m");
+                        if (k > 0)
+                                /* Current entry matches the one our cursor is pointing at, so let's try
+                                 * to advance the next entry. */
+                                r = sd_journal_step_one(j, !arg_reverse);
                 }
-        }
-
-        if (use_cursor) {
-                if (!arg_reverse)
-                        r = sd_journal_next_skip(j, 1 + after_cursor);
-                else
-                        r = sd_journal_previous_skip(j, 1 + after_cursor);
 
-                if (after_cursor && r < 2) {
+                if (r == 0) {
                         /* We couldn't find the next entry after the cursor. */
                         if (arg_follow)
                                 need_seek = true;
                         else
                                 arg_lines = 0;
                 }
-
-        } else if (arg_until_set && (arg_reverse || arg_lines >= 0)) {
-                /* If both --until and any of --reverse and --lines is specified, things get
+        } else if (arg_until_set && (arg_reverse || arg_lines_needs_seek_end())) {
+                /* If both --until and any of --reverse and --lines=N is specified, things get
                  * a little tricky. We seek to the place of --until first. If only --reverse or
                  * --reverse and --lines is specified, we search backwards and let the output
                  * counter handle --lines for us. If only --lines is used, we just jump backwards
@@ -2681,7 +2506,7 @@ static int run(int argc, char *argv[]) {
 
                 if (arg_reverse)
                         r = sd_journal_previous(j);
-                else /* arg_lines >= 0 */
+                else /* arg_lines_needs_seek_end */
                         r = sd_journal_previous_skip(j, arg_lines);
 
         } else if (arg_reverse) {
@@ -2691,7 +2516,7 @@ static int run(int argc, char *argv[]) {
 
                 r = sd_journal_previous(j);
 
-        } else if (arg_lines >= 0) {
+        } else if (arg_lines_needs_seek_end()) {
                 r = sd_journal_seek_tail(j);
                 if (r < 0)
                         return log_error_errno(r, "Failed to seek to tail: %m");