1 /*
2 // Copyright (c) 2017 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
17 #include "PresenceGpio.hpp"
18 #include "PwmSensor.hpp"
19 #include "TachSensor.hpp"
20 #include "Thresholds.hpp"
21 #include "Utils.hpp"
22 #include "VariantVisitors.hpp"
23
24 #include <boost/algorithm/string/replace.hpp>
25 #include <boost/asio/error.hpp>
26 #include <boost/asio/io_context.hpp>
27 #include <boost/asio/post.hpp>
28 #include <boost/asio/steady_timer.hpp>
29 #include <boost/container/flat_map.hpp>
30 #include <boost/container/flat_set.hpp>
31 #include <phosphor-logging/lg2.hpp>
32 #include <sdbusplus/asio/connection.hpp>
33 #include <sdbusplus/asio/object_server.hpp>
34 #include <sdbusplus/bus.hpp>
35 #include <sdbusplus/bus/match.hpp>
36 #include <sdbusplus/message.hpp>
37
38 #include <array>
39 #include <chrono>
40 #include <cstddef>
41 #include <cstdint>
42 #include <filesystem>
43 #include <fstream>
44 #include <functional>
45 #include <ios>
46 #include <map>
47 #include <memory>
48 #include <optional>
49 #include <regex>
50 #include <string>
51 #include <system_error>
52 #include <utility>
53 #include <variant>
54 #include <vector>
55
56 // The following two structures need to be consistent
57 static auto sensorTypes{std::to_array<const char*>(
58 {"AspeedFan", "I2CFan", "NuvotonFan", "HPEFan"})};
59
60 enum FanTypes
61 {
62 aspeed = 0,
63 i2c,
64 nuvoton,
65 hpe,
66 max,
67 };
68
69 static_assert(std::tuple_size<decltype(sensorTypes)>::value == FanTypes::max,
70 "sensorTypes element number is not equal to FanTypes number");
71
72 constexpr const char* redundancyConfiguration =
73 "xyz.openbmc_project.Configuration.FanRedundancy";
74 static std::regex inputRegex(R"(fan(\d+)_input)");
75
76 // todo: power supply fan redundancy
77 std::optional<RedundancySensor> systemRedundancy;
78
79 static const std::map<std::string, FanTypes> compatibleFanTypes = {
80 {"aspeed,ast2400-pwm-tacho", FanTypes::aspeed},
81 {"aspeed,ast2500-pwm-tacho", FanTypes::aspeed},
82 {"aspeed,ast2600-pwm-tach", FanTypes::aspeed},
83 {"aspeed,ast2700-pwm-tach", FanTypes::aspeed},
84 {"nuvoton,npcm750-pwm-fan", FanTypes::nuvoton},
85 {"nuvoton,npcm845-pwm-fan", FanTypes::nuvoton},
86 {"hpe,gxp-fan-ctrl", FanTypes::hpe}
87 // add compatible string here for new fan type
88 };
89
getFanType(const std::filesystem::path & parentPath)90 FanTypes getFanType(const std::filesystem::path& parentPath)
91 {
92 std::filesystem::path linkPath = parentPath / "of_node";
93 if (!std::filesystem::exists(linkPath))
94 {
95 return FanTypes::i2c;
96 }
97
98 std::string canonical = std::filesystem::canonical(linkPath);
99 std::string compatiblePath = canonical + "/compatible";
100 std::ifstream compatibleStream(compatiblePath);
101
102 if (!compatibleStream)
103 {
104 lg2::error("Error opening '{PATH}'", "PATH", compatiblePath);
105 return FanTypes::i2c;
106 }
107
108 std::string compatibleString;
109 while (std::getline(compatibleStream, compatibleString))
110 {
111 compatibleString.pop_back(); // trim EOL before comparisons
112
113 std::map<std::string, FanTypes>::const_iterator compatibleIterator =
114 compatibleFanTypes.find(compatibleString);
115
116 if (compatibleIterator != compatibleFanTypes.end())
117 {
118 return compatibleIterator->second;
119 }
120 }
121
122 return FanTypes::i2c;
123 }
enablePwm(const std::filesystem::path & filePath)124 void enablePwm(const std::filesystem::path& filePath)
125 {
126 std::fstream enableFile(filePath, std::ios::in | std::ios::out);
127 if (!enableFile.good())
128 {
129 lg2::error("Error read/write '{PATH}'", "PATH", filePath);
130 return;
131 }
132
133 std::string regulateMode;
134 std::getline(enableFile, regulateMode);
135 if (regulateMode == "0")
136 {
137 enableFile << 1;
138 }
139 }
findPwmfanPath(unsigned int configPwmfanIndex,std::filesystem::path & pwmPath)140 bool findPwmfanPath(unsigned int configPwmfanIndex,
141 std::filesystem::path& pwmPath)
142 {
143 /* Search PWM since pwm-fan had separated
144 * PWM from tach directory and 1 channel only*/
145 std::vector<std::filesystem::path> pwmfanPaths;
146 std::string pwnfanDevName("pwm-fan");
147
148 pwnfanDevName += std::to_string(configPwmfanIndex);
149
150 if (!findFiles(std::filesystem::path("/sys/class/hwmon"), R"(pwm\d+)",
151 pwmfanPaths))
152 {
153 lg2::error("No PWMs are found!");
154 return false;
155 }
156 for (const auto& path : pwmfanPaths)
157 {
158 std::error_code ec;
159 std::filesystem::path link =
160 std::filesystem::read_symlink(path.parent_path() / "device", ec);
161
162 if (ec)
163 {
164 lg2::error("read_symlink() failed: '{ERROR_MESSAGE}'",
165 "ERROR_MESSAGE", ec.message());
166 continue;
167 }
168
169 if (link.filename().string() == pwnfanDevName)
170 {
171 pwmPath = path;
172 return true;
173 }
174 }
175 return false;
176 }
findPwmPath(const std::filesystem::path & directory,unsigned int pwm,std::filesystem::path & pwmPath)177 bool findPwmPath(const std::filesystem::path& directory, unsigned int pwm,
178 std::filesystem::path& pwmPath)
179 {
180 std::error_code ec;
181
182 /* Assuming PWM file is appeared in the same directory as fanX_input */
183 auto path = directory / ("pwm" + std::to_string(pwm + 1));
184 bool exists = std::filesystem::exists(path, ec);
185
186 if (ec || !exists)
187 {
188 /* PWM file not exist or error happened */
189 if (ec)
190 {
191 lg2::error("exists() failed: '{ERROR_MESSAGE}'", "ERROR_MESSAGE",
192 ec.message());
193 }
194 /* try search form pwm-fanX directory */
195 return findPwmfanPath(pwm, pwmPath);
196 }
197
198 pwmPath = path;
199 return true;
200 }
201
202 // The argument to this function should be the fanN_input file that we want to
203 // enable. The function will locate the corresponding fanN_enable file if it
204 // exists. Note that some drivers don't provide this file if the sensors are
205 // always enabled.
enableFanInput(const std::filesystem::path & fanInputPath)206 void enableFanInput(const std::filesystem::path& fanInputPath)
207 {
208 std::error_code ec;
209 std::string path(fanInputPath.string());
210 boost::replace_last(path, "input", "enable");
211
212 bool exists = std::filesystem::exists(path, ec);
213 if (ec || !exists)
214 {
215 return;
216 }
217
218 std::fstream enableFile(path, std::ios::out);
219 if (!enableFile.good())
220 {
221 return;
222 }
223 enableFile << 1;
224 }
225
createRedundancySensor(const boost::container::flat_map<std::string,std::shared_ptr<TachSensor>> & sensors,const std::shared_ptr<sdbusplus::asio::connection> & conn,sdbusplus::asio::object_server & objectServer)226 void createRedundancySensor(
227 const boost::container::flat_map<std::string, std::shared_ptr<TachSensor>>&
228 sensors,
229 const std::shared_ptr<sdbusplus::asio::connection>& conn,
230 sdbusplus::asio::object_server& objectServer)
231 {
232 conn->async_method_call(
233 [&objectServer, &sensors](boost::system::error_code& ec,
234 const ManagedObjectType& managedObj) {
235 if (ec)
236 {
237 lg2::error("Error calling entity manager");
238 return;
239 }
240 for (const auto& [path, interfaces] : managedObj)
241 {
242 for (const auto& [intf, cfg] : interfaces)
243 {
244 if (intf == redundancyConfiguration)
245 {
246 // currently only support one
247 auto findCount = cfg.find("AllowedFailures");
248 if (findCount == cfg.end())
249 {
250 lg2::error("Malformed redundancy record");
251 return;
252 }
253 std::vector<std::string> sensorList;
254
255 for (const auto& [name, sensor] : sensors)
256 {
257 sensorList.push_back(
258 "/xyz/openbmc_project/sensors/fan_tach/" +
259 sensor->name);
260 }
261 systemRedundancy.reset();
262 systemRedundancy.emplace(RedundancySensor(
263 std::get<uint64_t>(findCount->second), sensorList,
264 objectServer, path));
265
266 return;
267 }
268 }
269 }
270 },
271 "xyz.openbmc_project.EntityManager", "/xyz/openbmc_project/inventory",
272 "org.freedesktop.DBus.ObjectManager", "GetManagedObjects");
273 }
274
createSensors(boost::asio::io_context & io,sdbusplus::asio::object_server & objectServer,boost::container::flat_map<std::string,std::shared_ptr<TachSensor>> & tachSensors,boost::container::flat_map<std::string,std::unique_ptr<PwmSensor>> & pwmSensors,boost::container::flat_map<std::string,std::weak_ptr<PresenceGpio>> & presenceGpios,std::shared_ptr<sdbusplus::asio::connection> & dbusConnection,const std::shared_ptr<boost::container::flat_set<std::string>> & sensorsChanged,size_t retries=0)275 void createSensors(
276 boost::asio::io_context& io, sdbusplus::asio::object_server& objectServer,
277 boost::container::flat_map<std::string, std::shared_ptr<TachSensor>>&
278 tachSensors,
279 boost::container::flat_map<std::string, std::unique_ptr<PwmSensor>>&
280 pwmSensors,
281 boost::container::flat_map<std::string, std::weak_ptr<PresenceGpio>>&
282 presenceGpios,
283 std::shared_ptr<sdbusplus::asio::connection>& dbusConnection,
284 const std::shared_ptr<boost::container::flat_set<std::string>>&
285 sensorsChanged,
286 size_t retries = 0)
287 {
288 auto getter = std::make_shared<
289 GetSensorConfiguration>(dbusConnection, [&io, &objectServer,
290 &tachSensors, &pwmSensors,
291 &presenceGpios,
292 &dbusConnection,
293 sensorsChanged](
294 const ManagedObjectType&
295 sensorConfigurations) {
296 bool firstScan = sensorsChanged == nullptr;
297 std::vector<std::filesystem::path> paths;
298 if (!findFiles(std::filesystem::path("/sys/class/hwmon"),
299 R"(fan\d+_input)", paths))
300 {
301 lg2::error("No fan sensors in system");
302 return;
303 }
304
305 // iterate through all found fan sensors, and try to match them with
306 // configuration
307 for (const auto& path : paths)
308 {
309 std::smatch match;
310 std::string pathStr = path.string();
311
312 std::regex_search(pathStr, match, inputRegex);
313 std::string indexStr = *(match.begin() + 1);
314
315 std::filesystem::path directory = path.parent_path();
316 FanTypes fanType = getFanType(directory);
317 std::string cfgIntf = configInterfaceName(sensorTypes[fanType]);
318
319 // convert to 0 based
320 size_t index = std::stoul(indexStr) - 1;
321
322 const char* baseType = nullptr;
323 const SensorData* sensorData = nullptr;
324 const std::string* interfacePath = nullptr;
325 const SensorBaseConfiguration* baseConfiguration = nullptr;
326 for (const auto& [path, cfgData] : sensorConfigurations)
327 {
328 // find the base of the configuration to see if indexes
329 // match
330 auto sensorBaseFind = cfgData.find(cfgIntf);
331 if (sensorBaseFind == cfgData.end())
332 {
333 continue;
334 }
335
336 baseConfiguration = &(*sensorBaseFind);
337 interfacePath = &path.str;
338 baseType = sensorTypes[fanType];
339
340 auto findIndex = baseConfiguration->second.find("Index");
341 if (findIndex == baseConfiguration->second.end())
342 {
343 lg2::error("'{INTERFACE}' missing index", "INTERFACE",
344 baseConfiguration->first);
345 continue;
346 }
347 unsigned int configIndex = std::visit(
348 VariantToUnsignedIntVisitor(), findIndex->second);
349 if (configIndex != index)
350 {
351 continue;
352 }
353 if (fanType == FanTypes::aspeed ||
354 fanType == FanTypes::nuvoton || fanType == FanTypes::hpe)
355 {
356 // there will be only 1 aspeed or nuvoton or hpe sensor
357 // object in sysfs, we found the fan
358 sensorData = &cfgData;
359 break;
360 }
361 if (fanType == FanTypes::i2c)
362 {
363 std::string deviceName =
364 std::filesystem::read_symlink(directory / "device")
365 .filename();
366
367 size_t bus = 0;
368 size_t addr = 0;
369 if (!getDeviceBusAddr(deviceName, bus, addr))
370 {
371 continue;
372 }
373
374 auto findBus = baseConfiguration->second.find("Bus");
375 auto findAddress =
376 baseConfiguration->second.find("Address");
377 if (findBus == baseConfiguration->second.end() ||
378 findAddress == baseConfiguration->second.end())
379 {
380 lg2::error("'{INTERFACE}' missing bus or address",
381 "INTERFACE", baseConfiguration->first);
382 continue;
383 }
384 unsigned int configBus = std::visit(
385 VariantToUnsignedIntVisitor(), findBus->second);
386 unsigned int configAddress = std::visit(
387 VariantToUnsignedIntVisitor(), findAddress->second);
388
389 if (configBus == bus && configAddress == addr)
390 {
391 sensorData = &cfgData;
392 break;
393 }
394 }
395 }
396 if (sensorData == nullptr)
397 {
398 lg2::error("failed to find match for '{PATH}'", "PATH",
399 path.string());
400 continue;
401 }
402
403 auto findSensorName = baseConfiguration->second.find("Name");
404
405 if (findSensorName == baseConfiguration->second.end())
406 {
407 lg2::error(
408 "could not determine configuration name for '{PATH}'",
409 "PATH", path.string());
410 continue;
411 }
412 std::string sensorName =
413 std::get<std::string>(findSensorName->second);
414
415 // on rescans, only update sensors we were signaled by
416 auto findSensor = tachSensors.find(sensorName);
417 if (!firstScan && findSensor != tachSensors.end())
418 {
419 bool found = false;
420 for (auto it = sensorsChanged->begin();
421 it != sensorsChanged->end(); it++)
422 {
423 if (it->ends_with(findSensor->second->name))
424 {
425 sensorsChanged->erase(it);
426 findSensor->second = nullptr;
427 found = true;
428 break;
429 }
430 }
431 if (!found)
432 {
433 continue;
434 }
435 }
436 std::vector<thresholds::Threshold> sensorThresholds;
437 if (!parseThresholdsFromConfig(*sensorData, sensorThresholds))
438 {
439 lg2::error("error populating thresholds for '{NAME}'", "NAME",
440 sensorName);
441 }
442
443 auto presenceConfig =
444 sensorData->find(cfgIntf + std::string(".Presence"));
445
446 std::shared_ptr<PresenceGpio> presenceGpio(nullptr);
447
448 // presence sensors are optional
449 if (presenceConfig != sensorData->end())
450 {
451 auto findPolarity = presenceConfig->second.find("Polarity");
452 auto findPinName = presenceConfig->second.find("PinName");
453
454 if (findPinName == presenceConfig->second.end() ||
455 findPolarity == presenceConfig->second.end())
456 {
457 lg2::error("Malformed Presence Configuration");
458 }
459 else
460 {
461 bool inverted =
462 std::get<std::string>(findPolarity->second) == "Low";
463 const auto* pinName =
464 std::get_if<std::string>(&findPinName->second);
465
466 if (pinName != nullptr)
467 {
468 auto findPresenceGpio = presenceGpios.find(*pinName);
469 if (findPresenceGpio != presenceGpios.end())
470 {
471 auto p = findPresenceGpio->second.lock();
472 if (p)
473 {
474 presenceGpio = p;
475 }
476 }
477 if (!presenceGpio)
478 {
479 auto findMonitorType =
480 presenceConfig->second.find("MonitorType");
481 bool polling = false;
482 if (findMonitorType != presenceConfig->second.end())
483 {
484 auto mType = std::get<std::string>(
485 findMonitorType->second);
486 if (mType == "Polling")
487 {
488 polling = true;
489 }
490 else if (mType != "Event")
491 {
492 lg2::error(
493 "Unsupported GPIO MonitorType of '{TYPE}' for '{NAME}', "
494 "supported types: Polling, Event default",
495 "TYPE", mType, "NAME", sensorName);
496 }
497 }
498 try
499 {
500 if (polling)
501 {
502 presenceGpio =
503 std::make_shared<PollingPresenceGpio>(
504 "Fan", sensorName, *pinName,
505 inverted, io);
506 }
507 else
508 {
509 presenceGpio =
510 std::make_shared<EventPresenceGpio>(
511 "Fan", sensorName, *pinName,
512 inverted, io);
513 }
514 presenceGpios[*pinName] = presenceGpio;
515 }
516 catch (const std::system_error& e)
517 {
518 lg2::error(
519 "Failed to create GPIO monitor object for "
520 "'{PIN_NAME}' / '{SENSOR_NAME}': '{ERROR}'",
521 "PIN_NAME", *pinName, "SENSOR_NAME",
522 sensorName, "ERROR", e);
523 }
524 }
525 }
526 else
527 {
528 lg2::error(
529 "Malformed Presence pinName for sensor '{NAME}'",
530 "NAME", sensorName);
531 }
532 }
533 }
534 std::optional<RedundancySensor>* redundancy = nullptr;
535 if (fanType == FanTypes::aspeed)
536 {
537 redundancy = &systemRedundancy;
538 }
539
540 PowerState powerState = getPowerState(baseConfiguration->second);
541
542 constexpr double defaultMaxReading = 25000;
543 constexpr double defaultMinReading = 0;
544 std::pair<double, double> limits =
545 std::make_pair(defaultMinReading, defaultMaxReading);
546
547 auto connector =
548 sensorData->find(cfgIntf + std::string(".Connector"));
549
550 std::optional<std::string> led;
551 std::string pwmName;
552 std::filesystem::path pwmPath;
553
554 // The Mutable parameter is optional, defaulting to false
555 bool isValueMutable = false;
556 if (connector != sensorData->end())
557 {
558 auto findPwm = connector->second.find("Pwm");
559 if (findPwm != connector->second.end())
560 {
561 size_t pwm = std::visit(VariantToUnsignedIntVisitor(),
562 findPwm->second);
563 if (!findPwmPath(directory, pwm, pwmPath))
564 {
565 lg2::error(
566 "Connector for '{NAME}' no pwm channel found!",
567 "NAME", sensorName);
568 continue;
569 }
570
571 std::filesystem::path pwmEnableFile =
572 "pwm" + std::to_string(pwm + 1) + "_enable";
573 std::filesystem::path enablePath =
574 pwmPath.parent_path() / pwmEnableFile;
575 enablePwm(enablePath);
576
577 /* use pwm name override if found in configuration else
578 * use default */
579 auto findOverride = connector->second.find("PwmName");
580 if (findOverride != connector->second.end())
581 {
582 pwmName = std::visit(VariantToStringVisitor(),
583 findOverride->second);
584 }
585 else
586 {
587 pwmName = "Pwm_" + std::to_string(pwm + 1);
588 }
589
590 // Check PWM sensor mutability
591 auto findMutable = connector->second.find("Mutable");
592 if (findMutable != connector->second.end())
593 {
594 const auto* ptrMutable =
595 std::get_if<bool>(&(findMutable->second));
596 if (ptrMutable != nullptr)
597 {
598 isValueMutable = *ptrMutable;
599 }
600 }
601 }
602 else
603 {
604 lg2::error("Connector for '{NAME}' missing pwm!", "NAME",
605 sensorName);
606 }
607
608 auto findLED = connector->second.find("LED");
609 if (findLED != connector->second.end())
610 {
611 const auto* ledName =
612 std::get_if<std::string>(&(findLED->second));
613 if (ledName == nullptr)
614 {
615 lg2::error("Wrong format for LED of '{NAME}'", "NAME",
616 sensorName);
617 }
618 else
619 {
620 led = *ledName;
621 }
622 }
623 }
624
625 findLimits(limits, baseConfiguration);
626
627 enableFanInput(path);
628
629 auto& tachSensor = tachSensors[sensorName];
630 tachSensor = nullptr;
631 tachSensor = std::make_shared<TachSensor>(
632 path.string(), baseType, objectServer, dbusConnection,
633 presenceGpio, redundancy, io, sensorName,
634 std::move(sensorThresholds), *interfacePath, limits, powerState,
635 led);
636 tachSensor->setupRead();
637
638 if (!pwmPath.empty() && std::filesystem::exists(pwmPath) &&
639 (pwmSensors.count(pwmPath) == 0U))
640 {
641 pwmSensors[pwmPath] = std::make_unique<PwmSensor>(
642 pwmName, pwmPath, dbusConnection, objectServer,
643 *interfacePath, "Fan", isValueMutable);
644 }
645 }
646
647 createRedundancySensor(tachSensors, dbusConnection, objectServer);
648 });
649 getter->getConfiguration(
650 std::vector<std::string>{sensorTypes.begin(), sensorTypes.end()},
651 retries);
652 }
653
main()654 int main()
655 {
656 boost::asio::io_context io;
657 auto systemBus = std::make_shared<sdbusplus::asio::connection>(io);
658 sdbusplus::asio::object_server objectServer(systemBus, true);
659
660 objectServer.add_manager("/xyz/openbmc_project/sensors");
661 objectServer.add_manager("/xyz/openbmc_project/control");
662 objectServer.add_manager("/xyz/openbmc_project/inventory");
663 systemBus->request_name("xyz.openbmc_project.FanSensor");
664 boost::container::flat_map<std::string, std::shared_ptr<TachSensor>>
665 tachSensors;
666 boost::container::flat_map<std::string, std::unique_ptr<PwmSensor>>
667 pwmSensors;
668 boost::container::flat_map<std::string, std::weak_ptr<PresenceGpio>>
669 presenceGpios;
670 auto sensorsChanged =
671 std::make_shared<boost::container::flat_set<std::string>>();
672
673 boost::asio::post(io, [&]() {
674 createSensors(io, objectServer, tachSensors, pwmSensors, presenceGpios,
675 systemBus, nullptr);
676 });
677
678 boost::asio::steady_timer filterTimer(io);
679 std::function<void(sdbusplus::message_t&)> eventHandler =
680 [&](sdbusplus::message_t& message) {
681 sensorsChanged->insert(message.get_path());
682 // this implicitly cancels the timer
683 filterTimer.expires_after(std::chrono::seconds(1));
684
685 filterTimer.async_wait([&](const boost::system::error_code& ec) {
686 if (ec == boost::asio::error::operation_aborted)
687 {
688 /* we were canceled*/
689 return;
690 }
691 if (ec)
692 {
693 lg2::error("timer error");
694 return;
695 }
696 createSensors(io, objectServer, tachSensors, pwmSensors,
697 presenceGpios, systemBus, sensorsChanged, 5);
698 });
699 };
700
701 std::vector<std::unique_ptr<sdbusplus::bus::match_t>> matches =
702 setupPropertiesChangedMatches(*systemBus, sensorTypes, eventHandler);
703
704 // redundancy sensor
705 std::function<void(sdbusplus::message_t&)> redundancyHandler =
706 [&tachSensors, &systemBus, &objectServer](sdbusplus::message_t&) {
707 createRedundancySensor(tachSensors, systemBus, objectServer);
708 };
709 auto match = std::make_unique<sdbusplus::bus::match_t>(
710 static_cast<sdbusplus::bus_t&>(*systemBus),
711 "type='signal',member='PropertiesChanged',path_namespace='" +
712 std::string(inventoryPath) + "',arg0namespace='" +
713 redundancyConfiguration + "'",
714 std::move(redundancyHandler));
715 matches.emplace_back(std::move(match));
716
717 setupManufacturingModeMatch(*systemBus);
718 io.run();
719 return 0;
720 }
721