Serenity Operating System
1/*
2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include <LibCore/ArgsParser.h>
8#include <LibCore/EventLoop.h>
9#include <LibCore/File.h>
10#include <LibCore/FileWatcher.h>
11#include <LibCore/System.h>
12
13#define DEFAULT_LINE_COUNT 10
14
15static ErrorOr<void> tail_from_pos(Core::File& file, off_t startline)
16{
17 TRY(file.seek(startline + 1, SeekMode::SetPosition));
18 auto buffer = TRY(file.read_until_eof());
19 out("{}", StringView { buffer });
20 return {};
21}
22
23static ErrorOr<off_t> find_seek_pos(Core::File& file, int wanted_lines)
24{
25 // Rather than reading the whole file, start at the end and work backwards,
26 // stopping when we've found the number of lines we want.
27 off_t pos = TRY(file.seek(0, SeekMode::FromEndPosition));
28
29 off_t end = pos;
30 int lines = 0;
31
32 for (; pos >= 0; pos--) {
33 TRY(file.seek(pos, SeekMode::SetPosition));
34
35 if (file.is_eof())
36 break;
37 auto ch = TRY(file.read_value<u8>());
38 if (ch == '\n' && (end - pos) > 1) {
39 lines++;
40 if (lines == wanted_lines)
41 break;
42 }
43 }
44
45 return pos;
46}
47
48ErrorOr<int> serenity_main(Main::Arguments arguments)
49{
50 TRY(Core::System::pledge("stdio rpath"));
51
52 bool follow = false;
53 size_t wanted_line_count = DEFAULT_LINE_COUNT;
54 StringView file;
55
56 Core::ArgsParser args_parser;
57 args_parser.set_general_help("Print the end ('tail') of a file.");
58 args_parser.add_option(follow, "Output data as it is written to the file", "follow", 'f');
59 args_parser.add_option(wanted_line_count, "Fetch the specified number of lines", "lines", 'n', "number");
60 args_parser.add_positional_argument(file, "File path", "file", Core::ArgsParser::Required::No);
61 args_parser.parse(arguments);
62
63 auto f = TRY(Core::File::open_file_or_standard_stream(file, Core::File::OpenMode::Read));
64 if (!follow)
65 TRY(Core::System::pledge("stdio"));
66
67 auto file_is_seekable = !f->tell().is_error();
68 if (!file_is_seekable) {
69 do {
70 // FIXME: If f is the standard input, f->read_all() does not block
71 // anymore after sending EOF (^D), despite f->is_open() returning true.
72 auto buffer = TRY(f->read_until_eof(PAGE_SIZE));
73 auto line_count = StringView(buffer).count("\n"sv);
74 auto bytes = buffer.bytes();
75 size_t line_index = 0;
76 StringBuilder line;
77
78 if (!line_count && wanted_line_count) {
79 out("{}", StringView { bytes });
80 continue;
81 }
82
83 for (size_t i = 0; i < bytes.size(); i++) {
84 auto ch = bytes.at(i);
85 line.append(ch);
86 if (ch == '\n') {
87 if (wanted_line_count > line_count || line_index >= line_count - wanted_line_count)
88 out("{}", line.to_deprecated_string());
89 line_index++;
90 line.clear();
91 }
92 }
93
94 // Since we can't have FileWatchers on the standard input either,
95 // we just loop forever if the -f option was passed.
96 } while (follow);
97 return 0;
98 }
99
100 auto pos = TRY(find_seek_pos(*f, wanted_line_count));
101 TRY(tail_from_pos(*f, pos));
102
103 if (follow) {
104 TRY(f->seek(0, SeekMode::FromEndPosition));
105
106 Core::EventLoop event_loop;
107 auto watcher = TRY(Core::FileWatcher::create());
108 watcher->on_change = [&](Core::FileWatcherEvent const& event) {
109 if (event.type == Core::FileWatcherEvent::Type::ContentModified) {
110 auto buffer_or_error = f->read_until_eof();
111 if (buffer_or_error.is_error()) {
112 auto error = buffer_or_error.release_error();
113 warnln(error.string_literal());
114 event_loop.quit(error.code());
115 return;
116 }
117 auto bytes = buffer_or_error.value().bytes();
118 out("{}", StringView { bytes });
119
120 auto potential_error = f->seek(0, SeekMode::FromEndPosition);
121 if (potential_error.is_error()) {
122 auto error = potential_error.release_error();
123 warnln(error.string_literal());
124 event_loop.quit(error.code());
125 return;
126 }
127 }
128 };
129 TRY(watcher->add_watch(file, Core::FileWatcherEvent::Type::ContentModified));
130 TRY(Core::System::pledge("stdio"));
131 return event_loop.exec();
132 }
133 return 0;
134}