From c8f790e4dd8e34673c2ca1c24e428157bd671b13 Mon Sep 17 00:00:00 2001 From: demostanis Date: Mon, 17 Oct 2022 21:24:52 +0200 Subject: [PATCH] tail: Port to Core::Stream, use Core::FileWatcher --- Userland/Utilities/tail.cpp | 148 +++++++++++++++++++++++------------- 1 file changed, 94 insertions(+), 54 deletions(-) diff --git a/Userland/Utilities/tail.cpp b/Userland/Utilities/tail.cpp index 5454ec1161..b5e8597fe4 100644 --- a/Userland/Utilities/tail.cpp +++ b/Userland/Utilities/tail.cpp @@ -5,67 +5,41 @@ */ #include -#include +#include +#include +#include #include -#include -#include -#include #define DEFAULT_LINE_COUNT 10 -static int tail_from_pos(Core::File& file, off_t startline, bool want_follow) +static ErrorOr tail_from_pos(Core::Stream::File& file, off_t startline) { - if (!file.seek(startline + 1)) - return 1; - - while (true) { - auto const& b = file.read(4096); - if (b.is_empty()) { - if (!want_follow) { - break; - } else { - while (!file.can_read()) { - // FIXME: would be nice to have access to can_read_from_fd with an infinite timeout - usleep(100); - } - continue; - } - } - - if (write(STDOUT_FILENO, b.data(), b.size()) < 0) - return 1; - } - - return 0; + TRY(file.seek(startline + 1, Core::Stream::SeekMode::SetPosition)); + auto buffer = TRY(file.read_all()); + out("{}", StringView { buffer }); + return {}; } -static off_t find_seek_pos(Core::File& file, int wanted_lines) +static ErrorOr find_seek_pos(Core::Stream::File& file, int wanted_lines) { // Rather than reading the whole file, start at the end and work backwards, // stopping when we've found the number of lines we want. - off_t pos = 0; - if (!file.seek(0, Core::SeekMode::FromEndPosition, &pos)) { - warnln("Failed to find end of file: {}", file.error_string()); - return 1; - } + off_t pos = TRY(file.seek(0, Core::Stream::SeekMode::FromEndPosition)); off_t end = pos; int lines = 0; - // FIXME: Reading char-by-char is only OK if IODevice's read buffer - // is smart enough to not read char-by-char. Fix it there, or fix it here :) for (; pos >= 0; pos--) { - file.seek(pos); - auto const& ch = file.read(1); - if (ch.is_empty()) { - // Presumably the file got truncated? - // Keep trying to read backwards... - } else { - if (*ch.data() == '\n' && (end - pos) > 1) { - lines++; - if (lines == wanted_lines) - break; - } + TRY(file.seek(pos, Core::Stream::SeekMode::SetPosition)); + + if (file.is_eof()) + break; + Array buffer; + auto ch = TRY(file.read(buffer)); + if (*ch.data() == '\n' && (end - pos) > 1) { + lines++; + if (lines == wanted_lines) + break; } } @@ -77,19 +51,85 @@ ErrorOr serenity_main(Main::Arguments arguments) TRY(Core::System::pledge("stdio rpath")); bool follow = false; - int line_count = DEFAULT_LINE_COUNT; - char const* file = nullptr; + size_t wanted_line_count = DEFAULT_LINE_COUNT; + StringView file; Core::ArgsParser args_parser; args_parser.set_general_help("Print the end ('tail') of a file."); args_parser.add_option(follow, "Output data as it is written to the file", "follow", 'f'); - args_parser.add_option(line_count, "Fetch the specified number of lines", "lines", 'n', "number"); - args_parser.add_positional_argument(file, "File path", "file"); + args_parser.add_option(wanted_line_count, "Fetch the specified number of lines", "lines", 'n', "number"); + args_parser.add_positional_argument(file, "File path", "file", Core::ArgsParser::Required::No); args_parser.parse(arguments); - auto f = TRY(Core::File::open(file, Core::OpenMode::ReadOnly)); - TRY(Core::System::pledge("stdio")); + auto f = TRY(Core::Stream::File::open_file_or_standard_stream(file, Core::Stream::OpenMode::Read)); + if (!follow) + TRY(Core::System::pledge("stdio")); - auto pos = find_seek_pos(*f, line_count); - return tail_from_pos(*f, pos, follow); + auto file_is_seekable = !f->tell().is_error(); + if (!file_is_seekable) { + do { + // FIXME: If f is the standard input, f->read_all() does not block + // anymore after sending EOF (^D), despite f->is_open() returning true. + auto buffer = TRY(f->read_all(PAGE_SIZE)); + auto line_count = StringView(buffer).count("\n"sv); + auto bytes = buffer.bytes(); + size_t line_index = 0; + StringBuilder line; + + if (!line_count && wanted_line_count) { + out("{}", StringView { bytes }); + continue; + } + + for (size_t i = 0; i < bytes.size(); i++) { + auto ch = bytes.at(i); + line.append(ch); + if (ch == '\n') { + if (wanted_line_count > line_count || line_index >= line_count - wanted_line_count) + out("{}", line.build()); + line_index++; + line.clear(); + } + } + + // Since we can't have FileWatchers on the standard input either, + // we just loop forever if the -f option was passed. + } while (follow); + return 0; + } + + auto pos = TRY(find_seek_pos(*f, wanted_line_count)); + TRY(tail_from_pos(*f, pos)); + + if (follow) { + TRY(f->seek(0, Core::Stream::SeekMode::FromEndPosition)); + + Core::EventLoop event_loop; + auto watcher = TRY(Core::FileWatcher::create()); + watcher->on_change = [&](Core::FileWatcherEvent const& event) { + if (event.type == Core::FileWatcherEvent::Type::ContentModified) { + auto buffer_or_error = f->read_all(); + if (buffer_or_error.is_error()) { + auto error = buffer_or_error.error(); + warnln(error.string_literal()); + event_loop.quit(error.code()); + return; + } + auto bytes = buffer_or_error.value().bytes(); + out("{}", StringView { bytes }); + + auto potential_error = f->seek(0, Core::Stream::SeekMode::FromEndPosition); + if (potential_error.is_error()) { + auto error = potential_error.error(); + warnln(error.string_literal()); + event_loop.quit(error.code()); + return; + } + } + }; + TRY(watcher->add_watch(file, Core::FileWatcherEvent::Type::ContentModified)); + TRY(Core::System::pledge("stdio")); + return event_loop.exec(); + } + return 0; }