blob: 1333de60e6d857c908e04c175ee9419b3f95be12 [file] [log] [blame]
Brian Silverman9891b292020-06-23 16:34:22 -07001#include <algorithm>
James Kuszmaul38735e82019-12-07 16:42:06 -08002#include <iostream>
Brian Silverman9891b292020-06-23 16:34:22 -07003#include <memory>
4#include <optional>
5#include <string>
6#include <string_view>
7#include <vector>
James Kuszmaul38735e82019-12-07 16:42:06 -08008
Austin Schuh0e8db662021-07-06 10:43:47 -07009#include "absl/strings/escaping.h"
James Kuszmaul38735e82019-12-07 16:42:06 -080010#include "aos/configuration.h"
Austin Schuhb06f03b2021-02-17 22:00:37 -080011#include "aos/events/logging/log_reader.h"
James Kuszmaul38735e82019-12-07 16:42:06 -080012#include "aos/events/simulated_event_loop.h"
13#include "aos/init.h"
14#include "aos/json_to_flatbuffer.h"
15#include "gflags/gflags.h"
16
James Kuszmaul38735e82019-12-07 16:42:06 -080017DEFINE_string(
18 name, "",
19 "Name to match for printing out channels. Empty means no name filter.");
20DEFINE_string(type, "",
21 "Channel type to match for printing out channels. Empty means no "
22 "type filter.");
Austin Schuh041fe9f2021-10-16 23:01:15 -070023DEFINE_bool(json, false, "If true, print fully valid JSON");
Austin Schuha81454b2020-05-12 19:58:36 -070024DEFINE_bool(fetch, false,
25 "If true, also print out the messages from before the start of the "
26 "log file");
Austin Schuh6f3babe2020-01-26 20:34:50 -080027DEFINE_bool(raw, false,
28 "If true, just print the data out unsorted and unparsed");
Brian Silverman8ff74aa2021-02-05 16:37:15 -080029DEFINE_string(raw_header, "",
30 "If set, the file to read the header from in raw mode");
Austin Schuhff3bc902022-05-11 16:10:57 -070031DEFINE_bool(distributed_clock, false,
32 "If true, print out the distributed time");
Austin Schuha81454b2020-05-12 19:58:36 -070033DEFINE_bool(format_raw, true,
34 "If true and --raw is specified, print out raw data, but use the "
35 "schema to format the data.");
Austin Schuhae46f362020-04-11 19:52:56 -070036DEFINE_int32(max_vector_size, 100,
37 "If positive, vectors longer than this will not be printed");
Ravago Jones5cc9df52020-09-02 21:29:58 -070038DEFINE_bool(pretty, false,
39 "If true, pretty print the messages on multiple lines");
Austin Schuh569c7f92020-12-11 20:01:42 -080040DEFINE_bool(print, true,
41 "If true, actually print the messages. If false, discard them, "
42 "confirming they can be parsed.");
Tyler Chatowee0afa82021-08-01 22:00:36 -070043DEFINE_uint64(
44 count, 0,
45 "If >0, log_cat will exit after printing this many messages. This "
46 "includes messages from before the start of the log if --fetch is set.");
Austin Schuh7af06d52021-06-28 15:46:59 -070047DEFINE_bool(print_parts_only, false,
48 "If true, only print out the results of logfile sorting.");
Austin Schuh25b17652021-07-21 15:42:56 -070049DEFINE_bool(channels, false,
50 "If true, print out all the configured channels for this log.");
Milind Upadhyay184dfda2022-03-26 15:54:38 -070051DEFINE_double(monotonic_start_time, 0.0,
52 "If set, only print messages sent at or after this many seconds "
53 "after epoch.");
54DEFINE_double(monotonic_end_time, 0.0,
55 "If set, only print messages sent at or before this many seconds "
56 "after epoch.");
Brian J Griglak043e0e22022-08-18 12:51:18 -060057DEFINE_bool(use_hex, false, "Are integers in the messages printed in hex notation.");
Austin Schuh6f3babe2020-01-26 20:34:50 -080058
Austin Schuh041fe9f2021-10-16 23:01:15 -070059using aos::monotonic_clock;
60namespace chrono = std::chrono;
61
62void StreamSeconds(std::ostream &stream,
63 const aos::monotonic_clock::time_point now) {
64 if (now < monotonic_clock::epoch()) {
65 chrono::seconds seconds =
66 chrono::duration_cast<chrono::seconds>(now.time_since_epoch());
67
68 stream << "-" << -seconds.count() << "." << std::setfill('0')
69 << std::setw(9)
70 << chrono::duration_cast<chrono::nanoseconds>(seconds -
71 now.time_since_epoch())
72 .count();
73 } else {
74 chrono::seconds seconds =
75 chrono::duration_cast<chrono::seconds>(now.time_since_epoch());
76 stream << seconds.count() << "." << std::setfill('0') << std::setw(9)
77 << chrono::duration_cast<chrono::nanoseconds>(
78 now.time_since_epoch() - seconds)
79 .count();
80 }
81}
82
Brian Silverman9891b292020-06-23 16:34:22 -070083// Print the flatbuffer out to stdout, both to remove the unnecessary cruft from
84// glog and to allow the user to readily redirect just the logged output
85// independent of any debugging information on stderr.
Austin Schuhff3bc902022-05-11 16:10:57 -070086void PrintMessage(const std::string_view node_name,
87 aos::NodeEventLoopFactory *node_factory,
88 const aos::Channel *channel, const aos::Context &context,
Austin Schuh746690f2020-08-01 16:15:57 -070089 aos::FastStringBuilder *builder) {
90 builder->Reset();
Austin Schuha9df9ad2021-06-16 14:49:39 -070091 CHECK(flatbuffers::Verify(*channel->schema(),
92 *channel->schema()->root_table(),
93 static_cast<const uint8_t *>(context.data),
94 static_cast<size_t>(context.size)))
95 << ": Corrupted flatbuffer on " << channel->name()->c_str() << " "
96 << channel->type()->c_str();
97
Ravago Jones5cc9df52020-09-02 21:29:58 -070098 aos::FlatbufferToJson(
99 builder, channel->schema(), static_cast<const uint8_t *>(context.data),
Brian J Griglak043e0e22022-08-18 12:51:18 -0600100 {.multi_line = FLAGS_pretty,
101 .max_vector_size = static_cast<size_t>(FLAGS_max_vector_size),
102 .max_multi_line = false,
103 .use_hex = FLAGS_use_hex});
Austin Schuh746690f2020-08-01 16:15:57 -0700104
Austin Schuh041fe9f2021-10-16 23:01:15 -0700105 if (FLAGS_json) {
106 std::cout << "{";
107 if (!node_name.empty()) {
108 std::cout << "\"node\": \"" << node_name << "\", ";
109 }
110 std::cout << "\"monotonic_event_time\": ";
111 StreamSeconds(std::cout, context.monotonic_event_time);
112 std::cout << ", \"realtime_event_time\": \"" << context.realtime_event_time
113 << "\", ";
114
115 if (context.monotonic_remote_time != context.monotonic_event_time) {
116 std::cout << "\"monotonic_remote_time\": ";
117 StreamSeconds(std::cout, context.monotonic_remote_time);
118 std::cout << ", \"realtime_remote_time\": \""
119 << context.realtime_remote_time << "\", ";
120 }
121
122 std::cout << "\"channel\": "
123 << aos::configuration::StrippedChannelToString(channel)
124 << ", \"data\": " << *builder << "}" << std::endl;
Austin Schuha81454b2020-05-12 19:58:36 -0700125 } else {
Austin Schuhff3bc902022-05-11 16:10:57 -0700126 if (FLAGS_distributed_clock) {
127 std::cout << node_factory->ToDistributedClock(
128 context.monotonic_event_time)
129 << " ";
130 }
Austin Schuh041fe9f2021-10-16 23:01:15 -0700131 if (!node_name.empty()) {
132 std::cout << node_name << " ";
133 }
134 if (context.monotonic_remote_time != context.monotonic_event_time) {
135 std::cout << context.realtime_event_time << " ("
136 << context.monotonic_event_time << ") sent "
137 << context.realtime_remote_time << " ("
138 << context.monotonic_remote_time << ") "
139 << channel->name()->c_str() << ' ' << channel->type()->c_str()
140 << ": " << *builder << std::endl;
141 } else {
142 std::cout << context.realtime_event_time << " ("
143 << context.monotonic_event_time << ") "
144 << channel->name()->c_str() << ' ' << channel->type()->c_str()
145 << ": " << *builder << std::endl;
146 }
Austin Schuha81454b2020-05-12 19:58:36 -0700147 }
148}
149
Austin Schuh58646e22021-08-23 23:51:46 -0700150// Prints out raw log parts to stdout.
151int PrintRaw(int argc, char **argv) {
152 if (argc != 2) {
153 LOG(FATAL) << "Expected 1 logfile as an argument.";
154 }
155 aos::logger::SpanReader reader(argv[1]);
156 absl::Span<const uint8_t> raw_log_file_header_span = reader.ReadMessage();
157
158 if (raw_log_file_header_span == absl::Span<const uint8_t>()) {
159 LOG(WARNING) << "Empty log file on " << reader.filename();
160 return 0;
161 }
162
163 // Now, reproduce the log file header deduplication logic inline so we can
164 // print out all the headers we find.
165 aos::SizePrefixedFlatbufferVector<aos::logger::LogFileHeader> log_file_header(
166 raw_log_file_header_span);
167 if (!log_file_header.Verify()) {
168 LOG(ERROR) << "Header corrupted on " << reader.filename();
169 return 1;
170 }
171 while (true) {
172 absl::Span<const uint8_t> maybe_header_data = reader.PeekMessage();
173 if (maybe_header_data == absl::Span<const uint8_t>()) {
174 break;
175 }
176
177 aos::SizePrefixedFlatbufferSpan<aos::logger::LogFileHeader> maybe_header(
178 maybe_header_data);
179 if (maybe_header.Verify()) {
180 std::cout << aos::FlatbufferToJson(
181 log_file_header, {.multi_line = FLAGS_pretty,
182 .max_vector_size = static_cast<size_t>(
183 FLAGS_max_vector_size)})
184 << std::endl;
185 LOG(WARNING) << "Found duplicate LogFileHeader in " << reader.filename();
186 log_file_header =
187 aos::SizePrefixedFlatbufferVector<aos::logger::LogFileHeader>(
188 maybe_header_data);
189
190 reader.ConsumeMessage();
191 } else {
192 break;
193 }
194 }
195
196 // And now use the final sha256 to match the raw_header.
197 std::optional<aos::logger::MessageReader> raw_header_reader;
198 const aos::logger::LogFileHeader *full_header = &log_file_header.message();
199 if (!FLAGS_raw_header.empty()) {
200 raw_header_reader.emplace(FLAGS_raw_header);
201 std::cout << aos::FlatbufferToJson(full_header,
202 {.multi_line = FLAGS_pretty,
203 .max_vector_size = static_cast<size_t>(
204 FLAGS_max_vector_size)})
205 << std::endl;
206 CHECK_EQ(
207 full_header->configuration_sha256()->string_view(),
208 aos::logger::Sha256(raw_header_reader->raw_log_file_header().span()));
209 full_header = raw_header_reader->log_file_header();
210 }
211
212 if (!FLAGS_print) {
213 return 0;
214 }
215
216 std::cout << aos::FlatbufferToJson(full_header,
217 {.multi_line = FLAGS_pretty,
218 .max_vector_size = static_cast<size_t>(
219 FLAGS_max_vector_size)})
220 << std::endl;
221 CHECK(full_header->has_configuration())
222 << ": Missing configuration! You may want to provide the path to the "
223 "logged configuration file using the --raw_header flag.";
224
225 while (true) {
226 const aos::SizePrefixedFlatbufferSpan<aos::logger::MessageHeader> message(
227 reader.ReadMessage());
228 if (message.span() == absl::Span<const uint8_t>()) {
229 break;
230 }
231 CHECK(message.Verify());
232
233 const auto *const channels = full_header->configuration()->channels();
234 const size_t channel_index = message.message().channel_index();
235 CHECK_LT(channel_index, channels->size());
236 const aos::Channel *const channel = channels->Get(channel_index);
237
238 CHECK(message.Verify()) << absl::BytesToHexString(
239 std::string_view(reinterpret_cast<const char *>(message.span().data()),
240 message.span().size()));
241
242 if (message.message().data() != nullptr) {
243 CHECK(channel->has_schema());
244
245 CHECK(flatbuffers::Verify(
246 *channel->schema(), *channel->schema()->root_table(),
247 message.message().data()->data(), message.message().data()->size()))
248 << ": Corrupted flatbuffer on " << channel->name()->c_str() << " "
249 << channel->type()->c_str();
250 }
251
252 if (FLAGS_format_raw && message.message().data() != nullptr) {
253 std::cout << aos::configuration::StrippedChannelToString(channel) << " "
254 << aos::FlatbufferToJson(message, {.multi_line = FLAGS_pretty,
255 .max_vector_size = 4})
256 << ": "
257 << aos::FlatbufferToJson(
258 channel->schema(), message.message().data()->data(),
259 {FLAGS_pretty,
260 static_cast<size_t>(FLAGS_max_vector_size)})
261 << std::endl;
262 } else {
263 std::cout << aos::configuration::StrippedChannelToString(channel) << " "
264 << aos::FlatbufferToJson(
265 message, {FLAGS_pretty,
266 static_cast<size_t>(FLAGS_max_vector_size)})
267 << std::endl;
268 }
269 }
270 return 0;
271}
272
273// This class prints out all data from a node on a boot.
274class NodePrinter {
275 public:
276 NodePrinter(aos::EventLoop *event_loop, uint64_t *message_print_counter,
277 aos::SimulatedEventLoopFactory *factory,
278 aos::FastStringBuilder *builder)
279 : factory_(factory),
Austin Schuhff3bc902022-05-11 16:10:57 -0700280 node_factory_(factory->GetNodeEventLoopFactory(event_loop->node())),
Austin Schuh58646e22021-08-23 23:51:46 -0700281 event_loop_(event_loop),
282 message_print_counter_(message_print_counter),
283 node_name_(
284 event_loop_->node() == nullptr
285 ? ""
Austin Schuh041fe9f2021-10-16 23:01:15 -0700286 : std::string(event_loop->node()->name()->string_view())),
Austin Schuh58646e22021-08-23 23:51:46 -0700287 builder_(builder) {
288 event_loop_->SkipTimingReport();
289 event_loop_->SkipAosLog();
290
291 const flatbuffers::Vector<flatbuffers::Offset<aos::Channel>> *channels =
292 event_loop_->configuration()->channels();
293
Milind Upadhyay184dfda2022-03-26 15:54:38 -0700294 const monotonic_clock::time_point start_time =
295 (FLAGS_monotonic_start_time == 0.0
296 ? monotonic_clock::min_time
297 : monotonic_clock::time_point(
298 std::chrono::duration_cast<monotonic_clock::duration>(
299 std::chrono::duration<double>(
300 FLAGS_monotonic_start_time))));
301 const monotonic_clock::time_point end_time =
302 (FLAGS_monotonic_end_time == 0.0
303 ? monotonic_clock::max_time
304 : monotonic_clock::time_point(
305 std::chrono::duration_cast<monotonic_clock::duration>(
306 std::chrono::duration<double>(
307 FLAGS_monotonic_end_time))));
308
Austin Schuh58646e22021-08-23 23:51:46 -0700309 for (flatbuffers::uoffset_t i = 0; i < channels->size(); i++) {
310 const aos::Channel *channel = channels->Get(i);
311 const flatbuffers::string_view name = channel->name()->string_view();
312 const flatbuffers::string_view type = channel->type()->string_view();
313 if (name.find(FLAGS_name) != std::string::npos &&
314 type.find(FLAGS_type) != std::string::npos) {
315 if (!aos::configuration::ChannelIsReadableOnNode(channel,
316 event_loop_->node())) {
317 continue;
318 }
319 VLOG(1) << "Listening on " << name << " " << type;
320
321 CHECK_NOTNULL(channel->schema());
Austin Schuh60e77942022-05-16 17:48:24 -0700322 event_loop_->MakeRawWatcher(channel, [this, channel, start_time,
323 end_time](
324 const aos::Context &context,
325 const void * /*message*/) {
326 if (!FLAGS_print) {
327 return;
328 }
Austin Schuh58646e22021-08-23 23:51:46 -0700329
Austin Schuh60e77942022-05-16 17:48:24 -0700330 if (!FLAGS_fetch && !started_) {
331 return;
332 }
Austin Schuh58646e22021-08-23 23:51:46 -0700333
Austin Schuh60e77942022-05-16 17:48:24 -0700334 if (context.monotonic_event_time < start_time ||
335 context.monotonic_event_time > end_time) {
336 return;
337 }
Milind Upadhyay184dfda2022-03-26 15:54:38 -0700338
Austin Schuh60e77942022-05-16 17:48:24 -0700339 PrintMessage(node_name_, node_factory_, channel, context, builder_);
340 ++(*message_print_counter_);
341 if (FLAGS_count > 0 && *message_print_counter_ >= FLAGS_count) {
342 factory_->Exit();
343 }
344 });
Austin Schuh58646e22021-08-23 23:51:46 -0700345 }
346 }
347 }
348
349 void SetStarted(bool started, aos::monotonic_clock::time_point monotonic_now,
350 aos::realtime_clock::time_point realtime_now) {
351 started_ = started;
Austin Schuh041fe9f2021-10-16 23:01:15 -0700352 if (FLAGS_json) {
353 return;
354 }
Austin Schuh58646e22021-08-23 23:51:46 -0700355 if (started_) {
356 std::cout << std::endl;
357 std::cout << (event_loop_->node() != nullptr
358 ? (event_loop_->node()->name()->str() + " ")
359 : "")
360 << "Log starting at " << realtime_now << " (" << monotonic_now
361 << ")";
362 std::cout << std::endl << std::endl;
363 } else {
364 std::cout << std::endl;
365 std::cout << (event_loop_->node() != nullptr
366 ? (event_loop_->node()->name()->str() + " ")
367 : "")
368 << "Log shutting down at " << realtime_now << " ("
369 << monotonic_now << ")";
370 std::cout << std::endl << std::endl;
371 }
372 }
373
374 private:
375 struct MessageInfo {
376 std::string node_name;
377 std::unique_ptr<aos::RawFetcher> fetcher;
378 };
379
380 aos::SimulatedEventLoopFactory *factory_;
Austin Schuhff3bc902022-05-11 16:10:57 -0700381 aos::NodeEventLoopFactory *node_factory_;
Austin Schuh58646e22021-08-23 23:51:46 -0700382 aos::EventLoop *event_loop_;
383
384 uint64_t *message_print_counter_ = nullptr;
385
386 std::string node_name_;
387
388 bool started_ = false;
389
390 aos::FastStringBuilder *builder_;
391};
392
James Kuszmaul38735e82019-12-07 16:42:06 -0800393int main(int argc, char **argv) {
394 gflags::SetUsageMessage(
Austin Schuh6f3babe2020-01-26 20:34:50 -0800395 "Usage:\n"
396 " log_cat [args] logfile1 logfile2 ...\n"
397 "\n"
James Kuszmaul38735e82019-12-07 16:42:06 -0800398 "This program provides a basic interface to dump data from a logfile to "
399 "stdout. Given a logfile, channel name filter, and type filter, it will "
400 "print all the messages in the logfile matching the filters. The message "
401 "filters work by taking the values of --name and --type and printing any "
402 "channel whose name contains --name as a substr and whose type contains "
403 "--type as a substr. Not specifying --name or --type leaves them free. "
404 "Calling this program without --name or --type specified prints out all "
405 "the logged data.");
406 aos::InitGoogle(&argc, &argv);
407
Austin Schuh6f3babe2020-01-26 20:34:50 -0800408 if (FLAGS_raw) {
Austin Schuh58646e22021-08-23 23:51:46 -0700409 return PrintRaw(argc, argv);
James Kuszmaul38735e82019-12-07 16:42:06 -0800410 }
411
Austin Schuh6f3babe2020-01-26 20:34:50 -0800412 if (argc < 2) {
413 LOG(FATAL) << "Expected at least 1 logfile as an argument.";
414 }
415
Austin Schuh11d43732020-09-21 17:28:30 -0700416 const std::vector<aos::logger::LogFile> logfiles =
Austin Schuh58646e22021-08-23 23:51:46 -0700417 aos::logger::SortParts(aos::logger::FindLogs(argc, argv));
Austin Schuh5212cad2020-09-09 23:12:09 -0700418
Austin Schuhfe3fb342021-01-16 18:50:37 -0800419 for (auto &it : logfiles) {
420 VLOG(1) << it;
Austin Schuh7af06d52021-06-28 15:46:59 -0700421 if (FLAGS_print_parts_only) {
422 std::cout << it << std::endl;
423 }
424 }
425 if (FLAGS_print_parts_only) {
426 return 0;
Austin Schuhfe3fb342021-01-16 18:50:37 -0800427 }
428
Austin Schuh6f3babe2020-01-26 20:34:50 -0800429 aos::logger::LogReader reader(logfiles);
Austin Schuha81454b2020-05-12 19:58:36 -0700430
Austin Schuh25b17652021-07-21 15:42:56 -0700431 if (FLAGS_channels) {
432 const aos::Configuration *config = reader.configuration();
433 for (const aos::Channel *channel : *config->channels()) {
434 std::cout << channel->name()->c_str() << " " << channel->type()->c_str()
435 << '\n';
436 }
437 return 0;
438 }
439
Austin Schuh58646e22021-08-23 23:51:46 -0700440 {
441 bool found_channel = false;
Austin Schuh6f3babe2020-01-26 20:34:50 -0800442 const flatbuffers::Vector<flatbuffers::Offset<aos::Channel>> *channels =
Austin Schuh58646e22021-08-23 23:51:46 -0700443 reader.configuration()->channels();
Brian Silverman9891b292020-06-23 16:34:22 -0700444
Austin Schuh6f3babe2020-01-26 20:34:50 -0800445 for (flatbuffers::uoffset_t i = 0; i < channels->size(); i++) {
446 const aos::Channel *channel = channels->Get(i);
447 const flatbuffers::string_view name = channel->name()->string_view();
448 const flatbuffers::string_view type = channel->type()->string_view();
449 if (name.find(FLAGS_name) != std::string::npos &&
450 type.find(FLAGS_type) != std::string::npos) {
Austin Schuh6f3babe2020-01-26 20:34:50 -0800451 found_channel = true;
452 }
453 }
Austin Schuh58646e22021-08-23 23:51:46 -0700454 if (!found_channel) {
455 LOG(FATAL) << "Could not find any channels";
Austin Schuha81454b2020-05-12 19:58:36 -0700456 }
James Kuszmaul38735e82019-12-07 16:42:06 -0800457 }
458
Austin Schuh58646e22021-08-23 23:51:46 -0700459 aos::FastStringBuilder builder;
James Kuszmaul912af072020-10-31 16:06:54 -0700460
Austin Schuh58646e22021-08-23 23:51:46 -0700461 uint64_t message_print_counter = 0;
462
463 std::vector<NodePrinter *> printers;
Sanjay Narayananbeb328c2021-09-01 16:24:20 -0700464 printers.resize(aos::configuration::NodesCount(reader.configuration()),
465 nullptr);
466
467 aos::SimulatedEventLoopFactory event_loop_factory(reader.configuration());
468
469 reader.RegisterWithoutStarting(&event_loop_factory);
Austin Schuh58646e22021-08-23 23:51:46 -0700470
471 for (const aos::Node *node :
472 aos::configuration::GetNodes(event_loop_factory.configuration())) {
473 size_t node_index = aos::configuration::GetNodeIndex(
474 event_loop_factory.configuration(), node);
475 // Spin up the printer, and hook up the SetStarted method so that it gets
476 // notified when the log starts and stops.
477 aos::NodeEventLoopFactory *node_factory =
478 event_loop_factory.GetNodeEventLoopFactory(node);
479 node_factory->OnStartup([&event_loop_factory, node_factory,
480 &message_print_counter, &builder, &printers,
481 node_index]() {
482 printers[node_index] = node_factory->AlwaysStart<NodePrinter>(
483 "printer", &message_print_counter, &event_loop_factory, &builder);
484 });
485 node_factory->OnShutdown(
486 [&printers, node_index]() { printers[node_index] = nullptr; });
487
488 reader.OnStart(node, [&printers, node_index, node_factory]() {
489 CHECK(printers[node_index]);
490 printers[node_index]->SetStarted(true, node_factory->monotonic_now(),
491 node_factory->realtime_now());
492 });
493 reader.OnEnd(node, [&printers, node_index, node_factory]() {
494 CHECK(printers[node_index]);
495 printers[node_index]->SetStarted(false, node_factory->monotonic_now(),
496 node_factory->realtime_now());
497 });
Austin Schuha81454b2020-05-12 19:58:36 -0700498 }
499
500 event_loop_factory.Run();
James Kuszmaul38735e82019-12-07 16:42:06 -0800501
Austin Schuh51a92592020-08-09 13:17:00 -0700502 reader.Deregister();
503
James Kuszmaul38735e82019-12-07 16:42:06 -0800504 return 0;
505}