blob: 4666dd7d62cdfef21e0aeb6e0f54c5242859428f [file] [log] [blame]
Jason M. Bills5e049d32018-10-19 12:59:38 -07001/*
2// Copyright (c) 2018 Intel Corporation
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15*/
16#include <systemd/sd-journal.h>
17
Ed Tanousc3526342023-03-06 13:37:53 -080018#include <boost/asio/io_context.hpp>
Jason M. Bills5e049d32018-10-19 12:59:38 -070019#include <boost/container/flat_map.hpp>
20#include <boost/container/flat_set.hpp>
Zhikui Ren672bdfc2020-07-14 11:37:01 -070021#include <pulse_event_monitor.hpp>
22#include <sdbusplus/asio/object_server.hpp>
23#include <sel_logger.hpp>
24#include <threshold_event_monitor.hpp>
Charles Hsudbd77b92020-10-29 11:20:34 +080025#include <watchdog_event_monitor.hpp>
Jonico Eustaquio9c495c62024-07-02 16:35:14 -050026#ifdef SEL_LOGGER_ENABLE_SEL_DELETE
27#include <xyz/openbmc_project/Common/error.hpp>
28#endif
George Hung486e42e2021-04-14 20:20:42 +080029#ifdef SEL_LOGGER_MONITOR_THRESHOLD_ALARM_EVENTS
30#include <threshold_alarm_event_monitor.hpp>
31#endif
JinFuLin7c2810b2022-12-02 13:55:28 +080032#ifdef SEL_LOGGER_MONITOR_HOST_ERROR_EVENTS
33#include <host_error_event_monitor.hpp>
34#endif
Zhikui Ren672bdfc2020-07-14 11:37:01 -070035
Jason M. Billsc4a336f2019-04-23 10:43:10 -070036#include <filesystem>
37#include <fstream>
Jason M. Bills5e049d32018-10-19 12:59:38 -070038#include <iomanip>
39#include <iostream>
Jason M. Bills5e049d32018-10-19 12:59:38 -070040#include <sstream>
Jason M. Bills5e049d32018-10-19 12:59:38 -070041
42struct DBusInternalError final : public sdbusplus::exception_t
43{
Zhikui Ren672bdfc2020-07-14 11:37:01 -070044 const char* name() const noexcept override
Jason M. Bills5e049d32018-10-19 12:59:38 -070045 {
46 return "org.freedesktop.DBus.Error.Failed";
Patrick Williamsa138ebd2021-09-08 15:46:34 -050047 }
Zhikui Ren672bdfc2020-07-14 11:37:01 -070048 const char* description() const noexcept override
Jason M. Bills5e049d32018-10-19 12:59:38 -070049 {
50 return "internal error";
Patrick Williamsa138ebd2021-09-08 15:46:34 -050051 }
Zhikui Ren672bdfc2020-07-14 11:37:01 -070052 const char* what() const noexcept override
Jason M. Bills5e049d32018-10-19 12:59:38 -070053 {
54 return "org.freedesktop.DBus.Error.Failed: "
55 "internal error";
Patrick Williamsa138ebd2021-09-08 15:46:34 -050056 }
57
58 int get_errno() const noexcept override
59 {
60 return EACCES;
61 }
Jason M. Bills5e049d32018-10-19 12:59:38 -070062};
63
Lei YUe526b862020-12-03 15:41:59 +080064#ifndef SEL_LOGGER_SEND_TO_LOGGING_SERVICE
Zhikui Ren672bdfc2020-07-14 11:37:01 -070065static bool getSELLogFiles(std::vector<std::filesystem::path>& selLogFiles)
Jason M. Billsc4a336f2019-04-23 10:43:10 -070066{
67 // Loop through the directory looking for ipmi_sel log files
Zhikui Ren672bdfc2020-07-14 11:37:01 -070068 for (const std::filesystem::directory_entry& dirEnt :
Jason M. Billsc4a336f2019-04-23 10:43:10 -070069 std::filesystem::directory_iterator(selLogDir))
70 {
71 std::string filename = dirEnt.path().filename();
George Liu27bfd422025-08-25 14:11:32 +080072 if (filename.starts_with(selLogFilename))
Jason M. Billsc4a336f2019-04-23 10:43:10 -070073 {
74 // If we find an ipmi_sel log file, save the path
75 selLogFiles.emplace_back(selLogDir / filename);
76 }
77 }
78 // As the log files rotate, they are appended with a ".#" that is higher for
79 // the older logs. Since we don't expect more than 10 log files, we
80 // can just sort the list to get them in order from newest to oldest
81 std::sort(selLogFiles.begin(), selLogFiles.end());
82
83 return !selLogFiles.empty();
84}
85
Jonico Eustaquio9fa224c2024-01-10 13:08:52 -060086static void saveClearSelTimestamp()
87{
88 int fd = open("/var/lib/ipmi/sel_erase_time",
89 O_WRONLY | O_CREAT | O_CLOEXEC, 0644);
90 if (fd < 0)
91 {
92 std::cerr << "Failed to open file\n";
93 return;
94 }
95
96 if (futimens(fd, NULL) < 0)
97 {
98 std::cerr << "Failed to update SEL cleared timestamp: "
99 << std::string(strerror(errno));
100 }
101 close(fd);
102}
103
104#ifdef SEL_LOGGER_ENABLE_SEL_DELETE
105std::vector<uint16_t> nextRecordsCache;
106
107static void backupCacheToFile()
108{
109 std::ofstream nextRecordStream(selLogDir / nextRecordFilename);
110 for (auto recordIds : nextRecordsCache)
111 {
112 nextRecordStream << recordIds << '\n';
113 }
114}
115
Lei YUb07851c2025-03-13 02:36:19 +0000116uint16_t getNewRecordId()
Jonico Eustaquio9fa224c2024-01-10 13:08:52 -0600117{
118 uint16_t nextRecordId = nextRecordsCache.back();
119 // Check if SEL is full
120 if (nextRecordId == selInvalidRecID)
121 {
122 return nextRecordId;
123 }
124 nextRecordsCache.pop_back();
125 if (nextRecordsCache.empty())
126 {
127 nextRecordsCache.push_back(nextRecordId + 1);
128 }
129 backupCacheToFile();
130 return nextRecordId;
131}
132
133static void initializeRecordId()
134{
135 std::ifstream nextRecordStream(selLogDir / nextRecordFilename);
136 if (!nextRecordStream.is_open())
137 {
138 std::ofstream newStream(selLogDir / nextRecordFilename);
139 newStream << '1' << '\n';
140 newStream.close();
141 nextRecordStream.open(selLogDir / nextRecordFilename);
142 }
143 std::string line;
144 while (std::getline(nextRecordStream, line))
145 {
146 nextRecordsCache.push_back(std::stoi(line));
147 }
148}
149
150void clearSelLogFiles()
151{
152 saveClearSelTimestamp();
153
154 // Clear the SEL by deleting the log files
155 std::vector<std::filesystem::path> selLogFiles;
156 if (getSELLogFiles(selLogFiles))
157 {
158 for (const std::filesystem::path& file : selLogFiles)
159 {
160 std::error_code ec;
161 std::filesystem::remove(file, ec);
162 }
163 }
164 // Reload rsyslog so it knows to start new log files
165 boost::asio::io_context io;
166 auto dbus = std::make_shared<sdbusplus::asio::connection>(io);
167 sdbusplus::message_t rsyslogReload = dbus->new_method_call(
168 "org.freedesktop.systemd1", "/org/freedesktop/systemd1",
169 "org.freedesktop.systemd1.Manager", "ReloadUnit");
170 rsyslogReload.append("rsyslog.service", "replace");
171 try
172 {
173 sdbusplus::message_t reloadResponse = dbus->call(rsyslogReload);
174 }
175 catch (const sdbusplus::exception_t& e)
176 {
177 std::cerr << e.what() << "\n";
178 }
179 // Set next record to 1
180 nextRecordsCache.clear();
181 nextRecordsCache.push_back(1);
182 // Update backup file as well
183 std::ofstream nextRecordStream(selLogDir / nextRecordFilename);
184 nextRecordStream << '1' << '\n';
185}
186
187static bool selDeleteTargetRecord(const uint16_t& targetId)
188{
189 bool targetEntryFound = false;
190 // Check if the ipmi_sel exist and save the path
191 std::vector<std::filesystem::path> selLogFiles;
192 if (!getSELLogFiles(selLogFiles))
193 {
194 return targetEntryFound;
195 }
196
197 // Go over all the ipmi_sel files to remove the entry with the target ID
198 for (const std::filesystem::path& file : selLogFiles)
199 {
200 std::fstream logStream(file, std::ios::in);
201 std::fstream tempFile(selLogDir / "temp", std::ios::out);
202 if (!logStream.is_open())
203 {
204 return targetEntryFound;
205 }
206 std::string line;
207 while (std::getline(logStream, line))
208 {
209 // Get the recordId of the current entry
210 int left = line.find(" ");
211 int right = line.find(",");
212 int recordLen = right - left;
213 std::string recordId = line.substr(left, recordLen);
214 int newRecordId = std::stoi(recordId);
215
216 if (newRecordId != targetId)
217 {
218 // Copy the entry from the original ipmi_sel to the temp file
219 tempFile << line << '\n';
220 }
221 else
222 {
223 // Skip copying the target entry
224 targetEntryFound = true;
225 }
226 }
227 logStream.close();
228 tempFile.close();
229 if (targetEntryFound)
230 {
231 std::fstream logStream(file, std::ios::out);
232 std::fstream tempFile(selLogDir / "temp", std::ios::in);
233 while (std::getline(tempFile, line))
234 {
235 logStream << line << '\n';
236 }
237 logStream.close();
238 tempFile.close();
239 std::error_code ec;
240 if (!std::filesystem::remove(selLogDir / "temp", ec))
241 {
242 std::cerr << ec.message() << std::endl;
243 }
244 break;
245 }
246 }
247 return targetEntryFound;
248}
249
Jonico Eustaquio9c495c62024-07-02 16:35:14 -0500250static void selDeleteRecord(const uint16_t& recordId)
Jonico Eustaquio9fa224c2024-01-10 13:08:52 -0600251{
252 std::filesystem::file_time_type prevAddTime =
253 std::filesystem::last_write_time(selLogDir / selLogFilename);
254 bool targetEntryFound = selDeleteTargetRecord(recordId);
255
256 // Check if the Record Id was found
257 if (!targetEntryFound)
258 {
Jonico Eustaquio9c495c62024-07-02 16:35:14 -0500259 throw sdbusplus::xyz::openbmc_project::Common::Error::
260 ResourceNotFound();
Jonico Eustaquio9fa224c2024-01-10 13:08:52 -0600261 }
262 // Add to next record cache for reuse
263 nextRecordsCache.push_back(recordId);
264 // Add to backup file
265 std::ofstream nextRecordStream(selLogDir / nextRecordFilename,
266 std::ios::app);
267 nextRecordStream << recordId << '\n';
268 // Keep Last Add Time the same
269 std::filesystem::last_write_time(selLogDir / selLogFilename, prevAddTime);
270 // Update Last Del Time
271 saveClearSelTimestamp();
Jonico Eustaquio9fa224c2024-01-10 13:08:52 -0600272}
273#else
Jonico Eustaquio0acff272024-04-23 15:12:06 -0500274static unsigned int initializeRecordId()
Jason M. Bills5e049d32018-10-19 12:59:38 -0700275{
Jason M. Billsc4a336f2019-04-23 10:43:10 -0700276 std::vector<std::filesystem::path> selLogFiles;
277 if (!getSELLogFiles(selLogFiles))
Jason M. Bills5e049d32018-10-19 12:59:38 -0700278 {
Jonico Eustaquio0acff272024-04-23 15:12:06 -0500279 return 0;
Jason M. Bills5e049d32018-10-19 12:59:38 -0700280 }
Jason M. Billsc4a336f2019-04-23 10:43:10 -0700281 std::ifstream logStream(selLogFiles.front());
282 if (!logStream.is_open())
Jason M. Bills5e049d32018-10-19 12:59:38 -0700283 {
Jonico Eustaquio0acff272024-04-23 15:12:06 -0500284 return 0;
Jason M. Billsc4a336f2019-04-23 10:43:10 -0700285 }
286 std::string line;
287 std::string newestEntry;
288 while (std::getline(logStream, line))
289 {
290 newestEntry = line;
291 }
Jason M. Bills5e049d32018-10-19 12:59:38 -0700292
Jason M. Billsc4a336f2019-04-23 10:43:10 -0700293 std::vector<std::string> newestEntryFields;
294 boost::split(newestEntryFields, newestEntry, boost::is_any_of(" ,"),
295 boost::token_compress_on);
296 if (newestEntryFields.size() < 4)
297 {
Jonico Eustaquio0acff272024-04-23 15:12:06 -0500298 return 0;
Jason M. Bills5e049d32018-10-19 12:59:38 -0700299 }
Jason M. Billsc4a336f2019-04-23 10:43:10 -0700300
301 return std::stoul(newestEntryFields[1]);
Jason M. Bills5e049d32018-10-19 12:59:38 -0700302}
303
Charles Boyer9f476e82021-07-29 16:33:01 -0500304static unsigned int recordId = initializeRecordId();
305
Lei YU3abf2b02025-03-21 05:51:37 +0000306unsigned int getNewRecordId()
Alexander Hansen8c023192023-09-26 09:15:18 +0200307{
Jonico Eustaquio9fa224c2024-01-10 13:08:52 -0600308 if (++recordId >= selInvalidRecID)
Alexander Hansen8c023192023-09-26 09:15:18 +0200309 {
Jonico Eustaquio9fa224c2024-01-10 13:08:52 -0600310 recordId = selInvalidRecID;
Alexander Hansen8c023192023-09-26 09:15:18 +0200311 }
Jonico Eustaquio9fa224c2024-01-10 13:08:52 -0600312 return recordId;
Alexander Hansen8c023192023-09-26 09:15:18 +0200313}
314
Charles Boyer9f476e82021-07-29 16:33:01 -0500315void clearSelLogFiles()
316{
Alexander Hansen8c023192023-09-26 09:15:18 +0200317 saveClearSelTimestamp();
318
Charles Boyer9f476e82021-07-29 16:33:01 -0500319 // Clear the SEL by deleting the log files
320 std::vector<std::filesystem::path> selLogFiles;
321 if (getSELLogFiles(selLogFiles))
322 {
323 for (const std::filesystem::path& file : selLogFiles)
324 {
325 std::error_code ec;
326 std::filesystem::remove(file, ec);
327 }
328 }
329
Jonico Eustaquio0acff272024-04-23 15:12:06 -0500330 recordId = 0;
Charles Boyer9f476e82021-07-29 16:33:01 -0500331
332 // Reload rsyslog so it knows to start new log files
Ed Tanousc3526342023-03-06 13:37:53 -0800333 boost::asio::io_context io;
Charles Boyer9f476e82021-07-29 16:33:01 -0500334 auto dbus = std::make_shared<sdbusplus::asio::connection>(io);
Patrick Williamsccef2272022-07-22 19:26:54 -0500335 sdbusplus::message_t rsyslogReload = dbus->new_method_call(
Charles Boyer9f476e82021-07-29 16:33:01 -0500336 "org.freedesktop.systemd1", "/org/freedesktop/systemd1",
337 "org.freedesktop.systemd1.Manager", "ReloadUnit");
338 rsyslogReload.append("rsyslog.service", "replace");
339 try
340 {
Patrick Williamsccef2272022-07-22 19:26:54 -0500341 sdbusplus::message_t reloadResponse = dbus->call(rsyslogReload);
Charles Boyer9f476e82021-07-29 16:33:01 -0500342 }
Patrick Williams3f4cd972021-10-06 12:42:50 -0500343 catch (const sdbusplus::exception_t& e)
Charles Boyer9f476e82021-07-29 16:33:01 -0500344 {
345 std::cerr << e.what() << "\n";
346 }
347}
Jonico Eustaquio9fa224c2024-01-10 13:08:52 -0600348#endif
Lei YUe526b862020-12-03 15:41:59 +0800349#endif
Jason M. Bills5e049d32018-10-19 12:59:38 -0700350
Lei YU9916d412025-02-06 11:51:18 +0000351void toHexStr(const std::vector<uint8_t>& data, std::string& hexStr)
Jason M. Bills5e049d32018-10-19 12:59:38 -0700352{
353 std::stringstream stream;
354 stream << std::hex << std::uppercase << std::setfill('0');
William A. Kennington III2e437262021-07-30 12:04:19 -0700355 for (int v : data)
Jason M. Bills5e049d32018-10-19 12:59:38 -0700356 {
357 stream << std::setw(2) << v;
358 }
359 hexStr = stream.str();
360}
361
Vincent Chou92721502024-01-24 15:09:24 -0600362static uint16_t selAddOemRecord(
Konstantin Aladyshev6f5342d2023-04-19 09:23:11 +0000363 [[maybe_unused]] std::shared_ptr<sdbusplus::asio::connection> conn,
364 [[maybe_unused]] const std::string& message,
365 const std::vector<uint8_t>& selData, const uint8_t& recordType)
Jason M. Bills5e049d32018-10-19 12:59:38 -0700366{
367 // A maximum of 13 bytes of SEL event data are allowed in an OEM record
368 if (selData.size() > selOemDataMaxSize)
369 {
370 throw std::invalid_argument("Event data too large");
371 }
372 std::string selDataStr;
373 toHexStr(selData, selDataStr);
374
Lei YUe526b862020-12-03 15:41:59 +0800375#ifdef SEL_LOGGER_SEND_TO_LOGGING_SERVICE
Konstantin Aladyshev6f5342d2023-04-19 09:23:11 +0000376 sdbusplus::message_t AddToLog = conn->new_method_call(
377 "xyz.openbmc_project.Logging", "/xyz/openbmc_project/logging",
378 "xyz.openbmc_project.Logging.Create", "Create");
379
380 std::string journalMsg(
381 message + ": " + " RecordType=" + std::to_string(recordType) +
382 ", GeneratorID=" + std::to_string(0) +
383 ", EventDir=" + std::to_string(0) + ", EventData=" + selDataStr);
384
Patrick Williams5a18f102024-08-16 15:20:38 -0400385 AddToLog.append(
386 journalMsg, "xyz.openbmc_project.Logging.Entry.Level.Informational",
387 std::map<std::string, std::string>(
388 {{"SENSOR_PATH", ""},
389 {"GENERATOR_ID", std::to_string(0)},
390 {"RECORD_TYPE", std::to_string(recordType)},
391 {"EVENT_DIR", std::to_string(0)},
392 {"SENSOR_DATA", selDataStr}}));
Konstantin Aladyshev6f5342d2023-04-19 09:23:11 +0000393 conn->call(AddToLog);
Vincent Chou92721502024-01-24 15:09:24 -0600394 return 0;
Lei YUe526b862020-12-03 15:41:59 +0800395#else
Jason M. Bills5e049d32018-10-19 12:59:38 -0700396 unsigned int recordId = getNewRecordId();
Jonico Eustaquio0acff272024-04-23 15:12:06 -0500397 if (recordId < selInvalidRecID)
398 {
399 sd_journal_send("MESSAGE=%s", message.c_str(), "PRIORITY=%i",
400 selPriority, "MESSAGE_ID=%s", selMessageId,
401 "IPMI_SEL_RECORD_ID=%d", recordId,
402 "IPMI_SEL_RECORD_TYPE=%x", recordType,
403 "IPMI_SEL_DATA=%s", selDataStr.c_str(), NULL);
404 }
Vincent Chou92721502024-01-24 15:09:24 -0600405 return recordId;
Lei YUe526b862020-12-03 15:41:59 +0800406#endif
Jason M. Bills5e049d32018-10-19 12:59:38 -0700407}
408
William A. Kennington IIId585c592021-07-30 12:10:25 -0700409int main(int, char*[])
Jason M. Bills5e049d32018-10-19 12:59:38 -0700410{
Jonico Eustaquio9fa224c2024-01-10 13:08:52 -0600411#ifndef SEL_LOGGER_SEND_TO_LOGGING_SERVICE
412#ifdef SEL_LOGGER_ENABLE_SEL_DELETE
413 initializeRecordId();
414#endif
415#endif
Jason M. Bills5e049d32018-10-19 12:59:38 -0700416 // setup connection to dbus
Ed Tanousc3526342023-03-06 13:37:53 -0800417 boost::asio::io_context io;
Jason M. Bills5e049d32018-10-19 12:59:38 -0700418 auto conn = std::make_shared<sdbusplus::asio::connection>(io);
419
420 // IPMI SEL Object
421 conn->request_name(ipmiSelObject);
422 auto server = sdbusplus::asio::object_server(conn);
423
424 // Add SEL Interface
425 std::shared_ptr<sdbusplus::asio::dbus_interface> ifaceAddSel =
426 server.add_interface(ipmiSelPath, ipmiSelAddInterface);
427
428 // Add a new SEL entry
429 ifaceAddSel->register_method(
Konstantin Aladyshev6f5342d2023-04-19 09:23:11 +0000430 "IpmiSelAdd",
431 [conn](const std::string& message, const std::string& path,
432 const std::vector<uint8_t>& selData, const bool& assert,
433 const uint16_t& genId) {
Patrick Williams5a18f102024-08-16 15:20:38 -0400434 return selAddSystemRecord(conn, message, path, selData, assert,
435 genId);
436 });
Jason M. Bills5e049d32018-10-19 12:59:38 -0700437 // Add a new OEM SEL entry
Patrick Williams5a18f102024-08-16 15:20:38 -0400438 ifaceAddSel->register_method(
439 "IpmiSelAddOem",
440 [conn](const std::string& message, const std::vector<uint8_t>& selData,
441 const uint8_t& recordType) {
442 return selAddOemRecord(conn, message, selData, recordType);
443 });
Charles Boyer9f476e82021-07-29 16:33:01 -0500444
Charles Boyer9f476e82021-07-29 16:33:01 -0500445#ifndef SEL_LOGGER_SEND_TO_LOGGING_SERVICE
446 // Clear SEL entries
447 ifaceAddSel->register_method("Clear", []() { clearSelLogFiles(); });
Jonico Eustaquio9fa224c2024-01-10 13:08:52 -0600448#ifdef SEL_LOGGER_ENABLE_SEL_DELETE
449 // Delete a SEL entry
Jonico Eustaquio9c495c62024-07-02 16:35:14 -0500450 ifaceAddSel->register_method("SELDelete", [](const uint16_t& recordId) {
Jonico Eustaquio9fa224c2024-01-10 13:08:52 -0600451 return selDeleteRecord(recordId);
452 });
453#endif
Charles Boyer9f476e82021-07-29 16:33:01 -0500454#endif
Jason M. Bills5e049d32018-10-19 12:59:38 -0700455 ifaceAddSel->initialize();
456
457#ifdef SEL_LOGGER_MONITOR_THRESHOLD_EVENTS
Patrick Williamsccef2272022-07-22 19:26:54 -0500458 sdbusplus::bus::match_t thresholdAssertMonitor =
Zhikui Ren25b26e12020-06-26 20:18:19 -0700459 startThresholdAssertMonitor(conn);
Jason M. Bills5e049d32018-10-19 12:59:38 -0700460#endif
461
Nikhil Potadeafbaa092019-03-06 16:18:13 -0800462#ifdef REDFISH_LOG_MONITOR_PULSE_EVENTS
Patrick Williamsccef2272022-07-22 19:26:54 -0500463 sdbusplus::bus::match_t pulseEventMonitor = startPulseEventMonitor(conn);
Nikhil Potadeafbaa092019-03-06 16:18:13 -0800464#endif
465
Charles Hsudbd77b92020-10-29 11:20:34 +0800466#ifdef SEL_LOGGER_MONITOR_WATCHDOG_EVENTS
Patrick Williamsccef2272022-07-22 19:26:54 -0500467 sdbusplus::bus::match_t watchdogEventMonitor =
Charles Hsudbd77b92020-10-29 11:20:34 +0800468 startWatchdogEventMonitor(conn);
469#endif
George Hung486e42e2021-04-14 20:20:42 +0800470
471#ifdef SEL_LOGGER_MONITOR_THRESHOLD_ALARM_EVENTS
472 startThresholdAlarmMonitor(conn);
473#endif
JinFuLin7c2810b2022-12-02 13:55:28 +0800474
475#ifdef SEL_LOGGER_MONITOR_HOST_ERROR_EVENTS
476 startHostErrorEventMonitor(conn);
477#endif
Jason M. Bills5e049d32018-10-19 12:59:38 -0700478 io.run();
479
480 return 0;
481}