12a40e939SJosh Lehan #include "ExternalSensor.hpp"
22a40e939SJosh Lehan #include "Utils.hpp"
32a40e939SJosh Lehan #include "VariantVisitors.hpp"
42a40e939SJosh Lehan 
52a40e939SJosh Lehan #include <boost/algorithm/string/replace.hpp>
62a40e939SJosh Lehan #include <boost/container/flat_map.hpp>
72a40e939SJosh Lehan #include <boost/container/flat_set.hpp>
82a40e939SJosh Lehan #include <sdbusplus/asio/connection.hpp>
92a40e939SJosh Lehan #include <sdbusplus/asio/object_server.hpp>
102a40e939SJosh Lehan #include <sdbusplus/bus/match.hpp>
112a40e939SJosh Lehan 
122a40e939SJosh Lehan #include <array>
132a40e939SJosh Lehan #include <filesystem>
142a40e939SJosh Lehan #include <fstream>
152a40e939SJosh Lehan #include <functional>
162a40e939SJosh Lehan #include <memory>
172a40e939SJosh Lehan #include <regex>
182a40e939SJosh Lehan #include <stdexcept>
192a40e939SJosh Lehan #include <string>
202a40e939SJosh Lehan #include <utility>
212a40e939SJosh Lehan #include <variant>
222a40e939SJosh Lehan #include <vector>
232a40e939SJosh Lehan 
242a40e939SJosh Lehan // Copied from HwmonTempSensor and inspired by
252a40e939SJosh Lehan // https://gerrit.openbmc-project.xyz/c/openbmc/dbus-sensors/+/35476
262a40e939SJosh Lehan 
272a40e939SJosh Lehan // The ExternalSensor is a sensor whose value is intended to be writable
282a40e939SJosh Lehan // by something external to the BMC, so that the host (or something else)
297243217bSJosh Lehan // can write to it, perhaps by using an IPMI or Redfish connection.
302a40e939SJosh Lehan 
312a40e939SJosh Lehan // Unlike most other sensors, an external sensor does not correspond
327243217bSJosh Lehan // to a hwmon file or any other kernel/hardware interface,
332a40e939SJosh Lehan // so, after initialization, this module does not have much to do,
342a40e939SJosh Lehan // but it handles reinitialization and thresholds, similar to the others.
357243217bSJosh Lehan // The main work of this module is to provide backing storage for a
367243217bSJosh Lehan // sensor that exists only virtually, and to provide an optional
377243217bSJosh Lehan // timeout service for detecting loss of timely updates.
382a40e939SJosh Lehan 
392a40e939SJosh Lehan // As there is no corresponding driver or hardware to support,
402a40e939SJosh Lehan // all configuration of this sensor comes from the JSON parameters:
417243217bSJosh Lehan // MinValue, MaxValue, Timeout, PowerState, Units, Name
422a40e939SJosh Lehan 
437243217bSJosh Lehan // The purpose of "Units" is to specify the physical characteristic
442a40e939SJosh Lehan // the external sensor is measuring, because with an external sensor
452a40e939SJosh Lehan // there is no other way to tell, and it will be used for the object path
467243217bSJosh Lehan // here: /xyz/openbmc_project/sensors/<Units>/<Name>
477243217bSJosh Lehan 
487243217bSJosh Lehan // For more information, see external-sensor.md design document:
497243217bSJosh Lehan // https://gerrit.openbmc-project.xyz/c/openbmc/docs/+/41452
507243217bSJosh Lehan // https://github.com/openbmc/docs/tree/master/designs/
512a40e939SJosh Lehan 
528a57ec09SEd Tanous static constexpr bool debug = false;
532a40e939SJosh Lehan 
54054aad8fSZev Weiss static const char* sensorType = "ExternalSensor";
552a40e939SJosh Lehan 
567243217bSJosh Lehan void updateReaper(boost::container::flat_map<
577243217bSJosh Lehan                       std::string, std::shared_ptr<ExternalSensor>>& sensors,
587243217bSJosh Lehan                   boost::asio::steady_timer& timer,
597243217bSJosh Lehan                   const std::chrono::steady_clock::time_point& now)
607243217bSJosh Lehan {
617243217bSJosh Lehan     // First pass, reap all stale sensors
6208cb50c5SZev Weiss     for (const auto& [name, sensor] : sensors)
637243217bSJosh Lehan     {
6408cb50c5SZev Weiss         if (!sensor)
657243217bSJosh Lehan         {
667243217bSJosh Lehan             continue;
677243217bSJosh Lehan         }
687243217bSJosh Lehan 
6908cb50c5SZev Weiss         if (!sensor->isAliveAndPerishable())
707243217bSJosh Lehan         {
717243217bSJosh Lehan             continue;
727243217bSJosh Lehan         }
737243217bSJosh Lehan 
7408cb50c5SZev Weiss         if (!sensor->isAliveAndFresh(now))
757243217bSJosh Lehan         {
767243217bSJosh Lehan             // Mark sensor as dead, no longer alive
7708cb50c5SZev Weiss             sensor->writeInvalidate();
787243217bSJosh Lehan         }
797243217bSJosh Lehan     }
807243217bSJosh Lehan 
817243217bSJosh Lehan     std::chrono::steady_clock::duration nextCheck;
827243217bSJosh Lehan     bool needCheck = false;
837243217bSJosh Lehan 
847243217bSJosh Lehan     // Second pass, determine timer interval to next check
8508cb50c5SZev Weiss     for (const auto& [name, sensor] : sensors)
867243217bSJosh Lehan     {
8708cb50c5SZev Weiss         if (!sensor)
887243217bSJosh Lehan         {
897243217bSJosh Lehan             continue;
907243217bSJosh Lehan         }
917243217bSJosh Lehan 
9208cb50c5SZev Weiss         if (!sensor->isAliveAndPerishable())
937243217bSJosh Lehan         {
947243217bSJosh Lehan             continue;
957243217bSJosh Lehan         }
967243217bSJosh Lehan 
9708cb50c5SZev Weiss         auto expiration = sensor->ageRemaining(now);
987243217bSJosh Lehan 
997243217bSJosh Lehan         if (needCheck)
1007243217bSJosh Lehan         {
1017243217bSJosh Lehan             nextCheck = std::min(nextCheck, expiration);
1027243217bSJosh Lehan         }
1037243217bSJosh Lehan         else
1047243217bSJosh Lehan         {
1057243217bSJosh Lehan             // Initialization
1067243217bSJosh Lehan             nextCheck = expiration;
1077243217bSJosh Lehan             needCheck = true;
1087243217bSJosh Lehan         }
1097243217bSJosh Lehan     }
1107243217bSJosh Lehan 
1117243217bSJosh Lehan     if (!needCheck)
1127243217bSJosh Lehan     {
1137243217bSJosh Lehan         if constexpr (debug)
1147243217bSJosh Lehan         {
1157243217bSJosh Lehan             std::cerr << "Next ExternalSensor timer idle\n";
1167243217bSJosh Lehan         }
1177243217bSJosh Lehan 
1187243217bSJosh Lehan         return;
1197243217bSJosh Lehan     }
1207243217bSJosh Lehan 
1217243217bSJosh Lehan     timer.expires_at(now + nextCheck);
1227243217bSJosh Lehan 
1237243217bSJosh Lehan     timer.async_wait([&sensors, &timer](const boost::system::error_code& err) {
1247243217bSJosh Lehan         if (err != boost::system::errc::success)
1257243217bSJosh Lehan         {
1267243217bSJosh Lehan             // Cancellation is normal, as timer is dynamically rescheduled
1270362738dSJosh Lehan             if (err != boost::asio::error::operation_aborted)
1287243217bSJosh Lehan             {
1297243217bSJosh Lehan                 std::cerr << "ExternalSensor timer scheduling problem: "
1307243217bSJosh Lehan                           << err.message() << "\n";
1317243217bSJosh Lehan             }
1327243217bSJosh Lehan             return;
1337243217bSJosh Lehan         }
1340362738dSJosh Lehan 
1357243217bSJosh Lehan         updateReaper(sensors, timer, std::chrono::steady_clock::now());
1367243217bSJosh Lehan     });
1377243217bSJosh Lehan 
1387243217bSJosh Lehan     if constexpr (debug)
1397243217bSJosh Lehan     {
1407243217bSJosh Lehan         std::cerr << "Next ExternalSensor timer "
1417243217bSJosh Lehan                   << std::chrono::duration_cast<std::chrono::microseconds>(
1427243217bSJosh Lehan                          nextCheck)
1437243217bSJosh Lehan                          .count()
1447243217bSJosh Lehan                   << " us\n";
1457243217bSJosh Lehan     }
1467243217bSJosh Lehan }
1477243217bSJosh Lehan 
1482a40e939SJosh Lehan void createSensors(
1498a17c303SEd Tanous     sdbusplus::asio::object_server& objectServer,
1502a40e939SJosh Lehan     boost::container::flat_map<std::string, std::shared_ptr<ExternalSensor>>&
1512a40e939SJosh Lehan         sensors,
1522a40e939SJosh Lehan     std::shared_ptr<sdbusplus::asio::connection>& dbusConnection,
1532a40e939SJosh Lehan     const std::shared_ptr<boost::container::flat_set<std::string>>&
1547243217bSJosh Lehan         sensorsChanged,
1557243217bSJosh Lehan     boost::asio::steady_timer& reaperTimer)
1562a40e939SJosh Lehan {
1570362738dSJosh Lehan     if constexpr (debug)
1580362738dSJosh Lehan     {
1590362738dSJosh Lehan         std::cerr << "ExternalSensor considering creating sensors\n";
1600362738dSJosh Lehan     }
1610362738dSJosh Lehan 
1622a40e939SJosh Lehan     auto getter = std::make_shared<GetSensorConfiguration>(
1632a40e939SJosh Lehan         dbusConnection,
1648a17c303SEd Tanous         [&objectServer, &sensors, &dbusConnection, sensorsChanged,
1657243217bSJosh Lehan          &reaperTimer](const ManagedObjectType& sensorConfigurations) {
1662a40e939SJosh Lehan         bool firstScan = (sensorsChanged == nullptr);
1672a40e939SJosh Lehan 
1682a40e939SJosh Lehan         for (const std::pair<sdbusplus::message::object_path, SensorData>&
1692a40e939SJosh Lehan                  sensor : sensorConfigurations)
1702a40e939SJosh Lehan         {
1712a40e939SJosh Lehan             const std::string& interfacePath = sensor.first.str;
1722a40e939SJosh Lehan             const SensorData& sensorData = sensor.second;
1732a40e939SJosh Lehan 
174054aad8fSZev Weiss             auto sensorBase = sensorData.find(configInterfaceName(sensorType));
1752a40e939SJosh Lehan             if (sensorBase == sensorData.end())
1762a40e939SJosh Lehan             {
1772a40e939SJosh Lehan                 std::cerr << "Base configuration not found for "
1782a40e939SJosh Lehan                           << interfacePath << "\n";
1792a40e939SJosh Lehan                 continue;
1802a40e939SJosh Lehan             }
1812a40e939SJosh Lehan 
1822a40e939SJosh Lehan             const SensorBaseConfiguration& baseConfiguration = *sensorBase;
183bb67932aSEd Tanous             const SensorBaseConfigMap& baseConfigMap = baseConfiguration.second;
1842a40e939SJosh Lehan 
1852a40e939SJosh Lehan             // MinValue and MinValue are mandatory numeric parameters
1862a40e939SJosh Lehan             auto minFound = baseConfigMap.find("MinValue");
1872a40e939SJosh Lehan             if (minFound == baseConfigMap.end())
1882a40e939SJosh Lehan             {
1892a40e939SJosh Lehan                 std::cerr << "MinValue parameter not found for "
1902a40e939SJosh Lehan                           << interfacePath << "\n";
1912a40e939SJosh Lehan                 continue;
1922a40e939SJosh Lehan             }
193a771f6a7SEd Tanous             double minValue =
1942a40e939SJosh Lehan                 std::visit(VariantToDoubleVisitor(), minFound->second);
1952a40e939SJosh Lehan             if (!std::isfinite(minValue))
1962a40e939SJosh Lehan             {
1972a40e939SJosh Lehan                 std::cerr << "MinValue parameter not parsed for "
1982a40e939SJosh Lehan                           << interfacePath << "\n";
1992a40e939SJosh Lehan                 continue;
2002a40e939SJosh Lehan             }
2012a40e939SJosh Lehan 
2022a40e939SJosh Lehan             auto maxFound = baseConfigMap.find("MaxValue");
2032a40e939SJosh Lehan             if (maxFound == baseConfigMap.end())
2042a40e939SJosh Lehan             {
2052a40e939SJosh Lehan                 std::cerr << "MaxValue parameter not found for "
2062a40e939SJosh Lehan                           << interfacePath << "\n";
2072a40e939SJosh Lehan                 continue;
2082a40e939SJosh Lehan             }
209a771f6a7SEd Tanous             double maxValue =
2102a40e939SJosh Lehan                 std::visit(VariantToDoubleVisitor(), maxFound->second);
2112a40e939SJosh Lehan             if (!std::isfinite(maxValue))
2122a40e939SJosh Lehan             {
2132a40e939SJosh Lehan                 std::cerr << "MaxValue parameter not parsed for "
2142a40e939SJosh Lehan                           << interfacePath << "\n";
2152a40e939SJosh Lehan                 continue;
2162a40e939SJosh Lehan             }
2172a40e939SJosh Lehan 
2187243217bSJosh Lehan             double timeoutSecs = 0.0;
2192a40e939SJosh Lehan 
2207243217bSJosh Lehan             // Timeout is an optional numeric parameter
2217243217bSJosh Lehan             auto timeoutFound = baseConfigMap.find("Timeout");
2227243217bSJosh Lehan             if (timeoutFound != baseConfigMap.end())
2237243217bSJosh Lehan             {
224bb67932aSEd Tanous                 timeoutSecs =
225bb67932aSEd Tanous                     std::visit(VariantToDoubleVisitor(), timeoutFound->second);
2267243217bSJosh Lehan             }
2277243217bSJosh Lehan             if (!(std::isfinite(timeoutSecs) && (timeoutSecs >= 0.0)))
2287243217bSJosh Lehan             {
2297243217bSJosh Lehan                 std::cerr << "Timeout parameter not parsed for "
2307243217bSJosh Lehan                           << interfacePath << "\n";
2317243217bSJosh Lehan                 continue;
2327243217bSJosh Lehan             }
2337243217bSJosh Lehan 
2347243217bSJosh Lehan             std::string sensorName;
2357243217bSJosh Lehan             std::string sensorUnits;
2367243217bSJosh Lehan 
2377243217bSJosh Lehan             // Name and Units are mandatory string parameters
2382a40e939SJosh Lehan             auto nameFound = baseConfigMap.find("Name");
2392a40e939SJosh Lehan             if (nameFound == baseConfigMap.end())
2402a40e939SJosh Lehan             {
241bb67932aSEd Tanous                 std::cerr << "Name parameter not found for " << interfacePath
242bb67932aSEd Tanous                           << "\n";
2432a40e939SJosh Lehan                 continue;
2442a40e939SJosh Lehan             }
2452a40e939SJosh Lehan             sensorName =
2462a40e939SJosh Lehan                 std::visit(VariantToStringVisitor(), nameFound->second);
2472a40e939SJosh Lehan             if (sensorName.empty())
2482a40e939SJosh Lehan             {
249bb67932aSEd Tanous                 std::cerr << "Name parameter not parsed for " << interfacePath
250bb67932aSEd Tanous                           << "\n";
2512a40e939SJosh Lehan                 continue;
2522a40e939SJosh Lehan             }
2532a40e939SJosh Lehan 
2547243217bSJosh Lehan             auto unitsFound = baseConfigMap.find("Units");
2557243217bSJosh Lehan             if (unitsFound == baseConfigMap.end())
2562a40e939SJosh Lehan             {
257bb67932aSEd Tanous                 std::cerr << "Units parameter not found for " << interfacePath
258bb67932aSEd Tanous                           << "\n";
2592a40e939SJosh Lehan                 continue;
2602a40e939SJosh Lehan             }
2617243217bSJosh Lehan             sensorUnits =
2627243217bSJosh Lehan                 std::visit(VariantToStringVisitor(), unitsFound->second);
2637243217bSJosh Lehan             if (sensorUnits.empty())
2642a40e939SJosh Lehan             {
265bb67932aSEd Tanous                 std::cerr << "Units parameter not parsed for " << interfacePath
266bb67932aSEd Tanous                           << "\n";
2672a40e939SJosh Lehan                 continue;
2682a40e939SJosh Lehan             }
2692a40e939SJosh Lehan 
2702a40e939SJosh Lehan             // on rescans, only update sensors we were signaled by
2712a40e939SJosh Lehan             auto findSensor = sensors.find(sensorName);
2722a40e939SJosh Lehan             if (!firstScan && (findSensor != sensors.end()))
2732a40e939SJosh Lehan             {
2742a40e939SJosh Lehan                 std::string suffixName = "/";
2752a40e939SJosh Lehan                 suffixName += findSensor->second->name;
2762a40e939SJosh Lehan                 bool found = false;
2772a40e939SJosh Lehan                 for (auto it = sensorsChanged->begin();
2782a40e939SJosh Lehan                      it != sensorsChanged->end(); it++)
2792a40e939SJosh Lehan                 {
2802a40e939SJosh Lehan                     std::string suffixIt = "/";
2812a40e939SJosh Lehan                     suffixIt += *it;
2826c106d66SZev Weiss                     if (suffixIt.ends_with(suffixName))
2832a40e939SJosh Lehan                     {
2842a40e939SJosh Lehan                         sensorsChanged->erase(it);
2852a40e939SJosh Lehan                         findSensor->second = nullptr;
2862a40e939SJosh Lehan                         found = true;
2870362738dSJosh Lehan                         if constexpr (debug)
2880362738dSJosh Lehan                         {
2890362738dSJosh Lehan                             std::cerr << "ExternalSensor " << sensorName
2900362738dSJosh Lehan                                       << " change found\n";
2910362738dSJosh Lehan                         }
2922a40e939SJosh Lehan                         break;
2932a40e939SJosh Lehan                     }
2942a40e939SJosh Lehan                 }
2952a40e939SJosh Lehan                 if (!found)
2962a40e939SJosh Lehan                 {
2972a40e939SJosh Lehan                     continue;
2982a40e939SJosh Lehan                 }
2992a40e939SJosh Lehan             }
3002a40e939SJosh Lehan 
3012a40e939SJosh Lehan             std::vector<thresholds::Threshold> sensorThresholds;
3022a40e939SJosh Lehan             if (!parseThresholdsFromConfig(sensorData, sensorThresholds))
3032a40e939SJosh Lehan             {
304bb67932aSEd Tanous                 std::cerr << "error populating thresholds for " << sensorName
305bb67932aSEd Tanous                           << "\n";
3062a40e939SJosh Lehan             }
3072a40e939SJosh Lehan 
308a4d2768cSZev Weiss             PowerState readState = getPowerState(baseConfigMap);
3092a40e939SJosh Lehan 
3102a40e939SJosh Lehan             auto& sensorEntry = sensors[sensorName];
3112a40e939SJosh Lehan             sensorEntry = nullptr;
3122a40e939SJosh Lehan 
3132a40e939SJosh Lehan             sensorEntry = std::make_shared<ExternalSensor>(
3142a40e939SJosh Lehan                 sensorType, objectServer, dbusConnection, sensorName,
3157243217bSJosh Lehan                 sensorUnits, std::move(sensorThresholds), interfacePath,
3160362738dSJosh Lehan                 maxValue, minValue, timeoutSecs, readState);
3170362738dSJosh Lehan             sensorEntry->initWriteHook(
3187243217bSJosh Lehan                 [&sensors, &reaperTimer](
3197243217bSJosh Lehan                     const std::chrono::steady_clock::time_point& now) {
3207243217bSJosh Lehan                 updateReaper(sensors, reaperTimer, now);
3217243217bSJosh Lehan             });
3227243217bSJosh Lehan 
3237243217bSJosh Lehan             if constexpr (debug)
3247243217bSJosh Lehan             {
325bb67932aSEd Tanous                 std::cerr << "ExternalSensor " << sensorName << " created\n";
3267243217bSJosh Lehan             }
3272a40e939SJosh Lehan         }
3282a40e939SJosh Lehan         });
3292a40e939SJosh Lehan 
3302a40e939SJosh Lehan     getter->getConfiguration(std::vector<std::string>{sensorType});
3312a40e939SJosh Lehan }
3322a40e939SJosh Lehan 
3332a40e939SJosh Lehan int main()
3342a40e939SJosh Lehan {
3357243217bSJosh Lehan     if constexpr (debug)
3367243217bSJosh Lehan     {
3377243217bSJosh Lehan         std::cerr << "ExternalSensor service starting up\n";
3387243217bSJosh Lehan     }
3397243217bSJosh Lehan 
3402a40e939SJosh Lehan     boost::asio::io_service io;
3412a40e939SJosh Lehan     auto systemBus = std::make_shared<sdbusplus::asio::connection>(io);
3422a40e939SJosh Lehan     systemBus->request_name("xyz.openbmc_project.ExternalSensor");
3432a40e939SJosh Lehan     sdbusplus::asio::object_server objectServer(systemBus);
3442a40e939SJosh Lehan     boost::container::flat_map<std::string, std::shared_ptr<ExternalSensor>>
3452a40e939SJosh Lehan         sensors;
3462a40e939SJosh Lehan     auto sensorsChanged =
3472a40e939SJosh Lehan         std::make_shared<boost::container::flat_set<std::string>>();
3487243217bSJosh Lehan     boost::asio::steady_timer reaperTimer(io);
3492a40e939SJosh Lehan 
3508a17c303SEd Tanous     io.post([&objectServer, &sensors, &systemBus, &reaperTimer]() {
3518a17c303SEd Tanous         createSensors(objectServer, sensors, systemBus, nullptr, reaperTimer);
3522a40e939SJosh Lehan     });
3532a40e939SJosh Lehan 
354*9b4a20e9SEd Tanous     boost::asio::steady_timer filterTimer(io);
35592f8f515SPatrick Williams     std::function<void(sdbusplus::message_t&)> eventHandler =
3568a17c303SEd Tanous         [&objectServer, &sensors, &systemBus, &sensorsChanged, &filterTimer,
35792f8f515SPatrick Williams          &reaperTimer](sdbusplus::message_t& message) mutable {
3582a40e939SJosh Lehan         if (message.is_method_error())
3592a40e939SJosh Lehan         {
3602a40e939SJosh Lehan             std::cerr << "callback method error\n";
3612a40e939SJosh Lehan             return;
3622a40e939SJosh Lehan         }
3630362738dSJosh Lehan 
3642049bd26SEd Tanous         const auto* messagePath = message.get_path();
3650362738dSJosh Lehan         sensorsChanged->insert(messagePath);
3660362738dSJosh Lehan         if constexpr (debug)
3670362738dSJosh Lehan         {
368bb67932aSEd Tanous             std::cerr << "ExternalSensor change event received: " << messagePath
369bb67932aSEd Tanous                       << "\n";
3700362738dSJosh Lehan         }
3710362738dSJosh Lehan 
3722a40e939SJosh Lehan         // this implicitly cancels the timer
373*9b4a20e9SEd Tanous         filterTimer.expires_from_now(std::chrono::seconds(1));
3742a40e939SJosh Lehan 
3758a17c303SEd Tanous         filterTimer.async_wait(
3768a17c303SEd Tanous             [&objectServer, &sensors, &systemBus, &sensorsChanged,
3778a17c303SEd Tanous              &reaperTimer](const boost::system::error_code& ec) mutable {
3780362738dSJosh Lehan             if (ec != boost::system::errc::success)
3792a40e939SJosh Lehan             {
3802a40e939SJosh Lehan                 if (ec != boost::asio::error::operation_aborted)
3812a40e939SJosh Lehan                 {
382bb67932aSEd Tanous                     std::cerr << "callback error: " << ec.message() << "\n";
3832a40e939SJosh Lehan                 }
3842a40e939SJosh Lehan                 return;
3852a40e939SJosh Lehan             }
3860362738dSJosh Lehan 
387bb67932aSEd Tanous             createSensors(objectServer, sensors, systemBus, sensorsChanged,
388bb67932aSEd Tanous                           reaperTimer);
3892a40e939SJosh Lehan         });
3902a40e939SJosh Lehan     };
3912a40e939SJosh Lehan 
392214d9717SZev Weiss     std::vector<std::unique_ptr<sdbusplus::bus::match_t>> matches =
393214d9717SZev Weiss         setupPropertiesChangedMatches(
394214d9717SZev Weiss             *systemBus, std::to_array<const char*>({sensorType}), eventHandler);
3952a40e939SJosh Lehan 
3967243217bSJosh Lehan     if constexpr (debug)
3977243217bSJosh Lehan     {
3987243217bSJosh Lehan         std::cerr << "ExternalSensor service entering main loop\n";
3997243217bSJosh Lehan     }
4007243217bSJosh Lehan 
4012a40e939SJosh Lehan     io.run();
4022a40e939SJosh Lehan }
403