xref: /openbmc/entity-manager/src/fru_device.cpp (revision 6eb60972)
1 /*
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 /// \file fru_device.cpp
17 
18 #include "fru_utils.hpp"
19 #include "utils.hpp"
20 
21 #include <fcntl.h>
22 #include <sys/inotify.h>
23 #include <sys/ioctl.h>
24 
25 #include <boost/algorithm/string/predicate.hpp>
26 #include <boost/asio/io_context.hpp>
27 #include <boost/asio/steady_timer.hpp>
28 #include <boost/container/flat_map.hpp>
29 #include <nlohmann/json.hpp>
30 #include <sdbusplus/asio/connection.hpp>
31 #include <sdbusplus/asio/object_server.hpp>
32 
33 #include <array>
34 #include <cerrno>
35 #include <charconv>
36 #include <chrono>
37 #include <ctime>
38 #include <filesystem>
39 #include <fstream>
40 #include <functional>
41 #include <future>
42 #include <iomanip>
43 #include <iostream>
44 #include <limits>
45 #include <map>
46 #include <regex>
47 #include <set>
48 #include <sstream>
49 #include <string>
50 #include <thread>
51 #include <utility>
52 #include <variant>
53 #include <vector>
54 
55 extern "C"
56 {
57 #include <i2c/smbus.h>
58 #include <linux/i2c-dev.h>
59 }
60 
61 namespace fs = std::filesystem;
62 static constexpr bool debug = false;
63 constexpr size_t maxFruSize = 512;
64 constexpr size_t maxEepromPageIndex = 255;
65 constexpr size_t busTimeoutSeconds = 10;
66 
67 constexpr const char* blacklistPath = PACKAGE_DIR "blacklist.json";
68 
69 const static constexpr char* baseboardFruLocation =
70     "/etc/fru/baseboard.fru.bin";
71 
72 const static constexpr char* i2CDevLocation = "/dev";
73 
74 static std::set<size_t> busBlacklist;
75 struct FindDevicesWithCallback;
76 
77 static boost::container::flat_map<
78     std::pair<size_t, size_t>, std::shared_ptr<sdbusplus::asio::dbus_interface>>
79     foundDevices;
80 
81 static boost::container::flat_map<size_t, std::set<size_t>> failedAddresses;
82 static boost::container::flat_map<size_t, std::set<size_t>> fruAddresses;
83 
84 boost::asio::io_context io;
85 
86 bool updateFRUProperty(
87     const std::string& updatePropertyReq, uint32_t bus, uint32_t address,
88     const std::string& propertyName,
89     boost::container::flat_map<
90         std::pair<size_t, size_t>,
91         std::shared_ptr<sdbusplus::asio::dbus_interface>>& dbusInterfaceMap,
92     size_t& unknownBusObjectCount, const bool& powerIsOn,
93     sdbusplus::asio::object_server& objServer,
94     std::shared_ptr<sdbusplus::asio::connection>& systemBus);
95 
96 // Given a bus/address, produce the path in sysfs for an eeprom.
97 static std::string getEepromPath(size_t bus, size_t address)
98 {
99     std::stringstream output;
100     output << "/sys/bus/i2c/devices/" << bus << "-" << std::right
101            << std::setfill('0') << std::setw(4) << std::hex << address
102            << "/eeprom";
103     return output.str();
104 }
105 
106 static bool hasEepromFile(size_t bus, size_t address)
107 {
108     auto path = getEepromPath(bus, address);
109     try
110     {
111         return fs::exists(path);
112     }
113     catch (...)
114     {
115         return false;
116     }
117 }
118 
119 static int64_t readFromEeprom(int fd, off_t offset, size_t len, uint8_t* buf)
120 {
121     auto result = lseek(fd, offset, SEEK_SET);
122     if (result < 0)
123     {
124         std::cerr << "failed to seek\n";
125         return -1;
126     }
127 
128     return read(fd, buf, len);
129 }
130 
131 static int busStrToInt(const std::string_view busName)
132 {
133     auto findBus = busName.rfind('-');
134     if (findBus == std::string::npos)
135     {
136         return -1;
137     }
138     std::string_view num = busName.substr(findBus + 1);
139     int val = 0;
140     std::from_chars(num.data(), num.data() + num.size(), val);
141     return val;
142 }
143 
144 static int getRootBus(size_t bus)
145 {
146     auto ec = std::error_code();
147     auto path = std::filesystem::read_symlink(
148         std::filesystem::path("/sys/bus/i2c/devices/i2c-" +
149                               std::to_string(bus) + "/mux_device"),
150         ec);
151     if (ec)
152     {
153         return -1;
154     }
155 
156     std::string filename = path.filename();
157     auto findBus = filename.find('-');
158     if (findBus == std::string::npos)
159     {
160         return -1;
161     }
162     return std::stoi(filename.substr(0, findBus));
163 }
164 
165 static bool isMuxBus(size_t bus)
166 {
167     auto ec = std::error_code();
168     auto isSymlink =
169         is_symlink(std::filesystem::path("/sys/bus/i2c/devices/i2c-" +
170                                          std::to_string(bus) + "/mux_device"),
171                    ec);
172     return (!ec && isSymlink);
173 }
174 
175 static void makeProbeInterface(size_t bus, size_t address,
176                                sdbusplus::asio::object_server& objServer)
177 {
178     if (isMuxBus(bus))
179     {
180         return; // the mux buses are random, no need to publish
181     }
182     auto [it, success] = foundDevices.emplace(
183         std::make_pair(bus, address),
184         objServer.add_interface(
185             "/xyz/openbmc_project/FruDevice/" + std::to_string(bus) + "_" +
186                 std::to_string(address),
187             "xyz.openbmc_project.Inventory.Item.I2CDevice"));
188     if (!success)
189     {
190         return; // already added
191     }
192     it->second->register_property("Bus", bus);
193     it->second->register_property("Address", address);
194     it->second->initialize();
195 }
196 
197 static std::optional<bool> isDevice16Bit(int file)
198 {
199     // Set the higher data word address bits to 0. It's safe on 8-bit addressing
200     // EEPROMs because it doesn't write any actual data.
201     int ret = i2c_smbus_write_byte(file, 0);
202     if (ret < 0)
203     {
204         return std::nullopt;
205     }
206 
207     /* Get first byte */
208     int byte1 = i2c_smbus_read_byte_data(file, 0);
209     if (byte1 < 0)
210     {
211         return std::nullopt;
212     }
213     /* Read 7 more bytes, it will read same first byte in case of
214      * 8 bit but it will read next byte in case of 16 bit
215      */
216     for (int i = 0; i < 7; i++)
217     {
218         int byte2 = i2c_smbus_read_byte_data(file, 0);
219         if (byte2 < 0)
220         {
221             return std::nullopt;
222         }
223         if (byte2 != byte1)
224         {
225             return true;
226         }
227     }
228     return false;
229 }
230 
231 // Issue an I2C transaction to first write to_slave_buf_len bytes,then read
232 // from_slave_buf_len bytes.
233 static int i2cSmbusWriteThenRead(int file, uint16_t address,
234                                  uint8_t* toSlaveBuf, uint8_t toSlaveBufLen,
235                                  uint8_t* fromSlaveBuf, uint8_t fromSlaveBufLen)
236 {
237     if (toSlaveBuf == nullptr || toSlaveBufLen == 0 ||
238         fromSlaveBuf == nullptr || fromSlaveBufLen == 0)
239     {
240         return -1;
241     }
242 
243     constexpr size_t smbusWriteThenReadMsgCount = 2;
244     std::array<struct i2c_msg, smbusWriteThenReadMsgCount> msgs{};
245     struct i2c_rdwr_ioctl_data rdwr
246     {};
247 
248     msgs[0].addr = address;
249     msgs[0].flags = 0;
250     msgs[0].len = toSlaveBufLen;
251     msgs[0].buf = toSlaveBuf;
252     msgs[1].addr = address;
253     msgs[1].flags = I2C_M_RD;
254     msgs[1].len = fromSlaveBufLen;
255     msgs[1].buf = fromSlaveBuf;
256 
257     rdwr.msgs = msgs.data();
258     rdwr.nmsgs = msgs.size();
259 
260     int ret = ioctl(file, I2C_RDWR, &rdwr);
261 
262     return (ret == static_cast<int>(msgs.size())) ? msgs[1].len : -1;
263 }
264 
265 static int64_t readData(bool is16bit, bool isBytewise, int file,
266                         uint16_t address, off_t offset, size_t len,
267                         uint8_t* buf)
268 {
269     if (!is16bit)
270     {
271         if (!isBytewise)
272         {
273             return i2c_smbus_read_i2c_block_data(
274                 file, static_cast<uint8_t>(offset), len, buf);
275         }
276 
277         std::span<uint8_t> bufspan{buf, len};
278         for (size_t i = 0; i < len; i++)
279         {
280             int byte = i2c_smbus_read_byte_data(
281                 file, static_cast<uint8_t>(offset + i));
282             if (byte < 0)
283             {
284                 return static_cast<int64_t>(byte);
285             }
286             bufspan[i] = static_cast<uint8_t>(byte);
287         }
288         return static_cast<int64_t>(len);
289     }
290 
291     offset = htobe16(offset);
292     // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
293     uint8_t* u8Offset = reinterpret_cast<uint8_t*>(&offset);
294     return i2cSmbusWriteThenRead(file, address, u8Offset, 2, buf, len);
295 }
296 
297 // TODO: This code is very similar to the non-eeprom version and can be merged
298 // with some tweaks.
299 static std::vector<uint8_t> processEeprom(int bus, int address)
300 {
301     auto path = getEepromPath(bus, address);
302 
303     int file = open(path.c_str(), O_RDONLY);
304     if (file < 0)
305     {
306         std::cerr << "Unable to open eeprom file: " << path << "\n";
307         return {};
308     }
309 
310     std::string errorMessage = "eeprom at " + std::to_string(bus) +
311                                " address " + std::to_string(address);
312     auto readFunc = [file](off_t offset, size_t length, uint8_t* outbuf) {
313         return readFromEeprom(file, offset, length, outbuf);
314     };
315     FRUReader reader(std::move(readFunc));
316     std::pair<std::vector<uint8_t>, bool> pair = readFRUContents(reader,
317                                                                  errorMessage);
318 
319     close(file);
320     return pair.first;
321 }
322 
323 std::set<int> findI2CEeproms(int i2cBus,
324                              const std::shared_ptr<DeviceMap>& devices)
325 {
326     std::set<int> foundList;
327 
328     std::string path = "/sys/bus/i2c/devices/i2c-" + std::to_string(i2cBus);
329 
330     // For each file listed under the i2c device
331     // NOTE: This should be faster than just checking for each possible address
332     // path.
333     auto ec = std::error_code();
334     for (const auto& p : fs::directory_iterator(path, ec))
335     {
336         if (ec)
337         {
338             std::cerr << "directory_iterator err " << ec.message() << "\n";
339             break;
340         }
341         const std::string node = p.path().string();
342         std::smatch m;
343         bool found = std::regex_match(node, m,
344                                       std::regex(".+\\d+-([0-9abcdef]+$)"));
345 
346         if (!found)
347         {
348             continue;
349         }
350         if (m.size() != 2)
351         {
352             std::cerr << "regex didn't capture\n";
353             continue;
354         }
355 
356         std::ssub_match subMatch = m[1];
357         std::string addressString = subMatch.str();
358 
359         std::size_t ignored = 0;
360         const int hexBase = 16;
361         int address = std::stoi(addressString, &ignored, hexBase);
362 
363         const std::string eeprom = node + "/eeprom";
364 
365         try
366         {
367             if (!fs::exists(eeprom))
368             {
369                 continue;
370             }
371         }
372         catch (...)
373         {
374             continue;
375         }
376 
377         // There is an eeprom file at this address, it may have invalid
378         // contents, but we found it.
379         foundList.insert(address);
380 
381         std::vector<uint8_t> device = processEeprom(i2cBus, address);
382         if (!device.empty())
383         {
384             devices->emplace(address, device);
385         }
386     }
387 
388     return foundList;
389 }
390 
391 int getBusFRUs(int file, int first, int last, int bus,
392                std::shared_ptr<DeviceMap> devices, const bool& powerIsOn,
393                sdbusplus::asio::object_server& objServer)
394 {
395     std::future<int> future = std::async(std::launch::async, [&]() {
396         // NOTE: When reading the devices raw on the bus, it can interfere with
397         // the driver's ability to operate, therefore read eeproms first before
398         // scanning for devices without drivers. Several experiments were run
399         // and it was determined that if there were any devices on the bus
400         // before the eeprom was hit and read, the eeprom driver wouldn't open
401         // while the bus device was open. An experiment was not performed to see
402         // if this issue was resolved if the i2c bus device was closed, but
403         // hexdumps of the eeprom later were successful.
404 
405         // Scan for i2c eeproms loaded on this bus.
406         std::set<int> skipList = findI2CEeproms(bus, devices);
407         std::set<size_t>& failedItems = failedAddresses[bus];
408         std::set<size_t>& foundItems = fruAddresses[bus];
409         foundItems.clear();
410 
411         std::set<size_t>* rootFailures = nullptr;
412         int rootBus = getRootBus(bus);
413 
414         if (rootBus >= 0)
415         {
416             rootFailures = &(failedAddresses[rootBus]);
417             foundItems = fruAddresses[rootBus];
418         }
419 
420         constexpr int startSkipSlaveAddr = 0;
421         constexpr int endSkipSlaveAddr = 12;
422 
423         for (int ii = first; ii <= last; ii++)
424         {
425             if (foundItems.find(ii) != foundItems.end())
426             {
427                 continue;
428             }
429             if (skipList.find(ii) != skipList.end())
430             {
431                 continue;
432             }
433             // skipping since no device is present in this range
434             if (ii >= startSkipSlaveAddr && ii <= endSkipSlaveAddr)
435             {
436                 continue;
437             }
438             // Set slave address
439             if (ioctl(file, I2C_SLAVE, ii) < 0)
440             {
441                 std::cerr << "device at bus " << bus << " address " << ii
442                           << " busy\n";
443                 continue;
444             }
445             // probe
446             if (i2c_smbus_read_byte(file) < 0)
447             {
448                 continue;
449             }
450 
451             if (debug)
452             {
453                 std::cout << "something at bus " << bus << " addr " << ii
454                           << "\n";
455             }
456 
457             makeProbeInterface(bus, ii, objServer);
458 
459             if (failedItems.find(ii) != failedItems.end())
460             {
461                 // if we failed to read it once, unlikely we can read it later
462                 continue;
463             }
464 
465             if (rootFailures != nullptr)
466             {
467                 if (rootFailures->find(ii) != rootFailures->end())
468                 {
469                     continue;
470                 }
471             }
472 
473             /* Check for Device type if it is 8 bit or 16 bit */
474             std::optional<bool> is16Bit = isDevice16Bit(file);
475             if (!is16Bit.has_value())
476             {
477                 std::cerr << "failed to read bus " << bus << " address " << ii
478                           << "\n";
479                 if (powerIsOn)
480                 {
481                     failedItems.insert(ii);
482                 }
483                 continue;
484             }
485             bool is16BitBool{*is16Bit};
486 
487             auto readFunc = [is16BitBool, file, ii](off_t offset, size_t length,
488                                                     uint8_t* outbuf) {
489                 return readData(is16BitBool, false, file, ii, offset, length,
490                                 outbuf);
491             };
492             FRUReader reader(std::move(readFunc));
493             std::string errorMessage = "bus " + std::to_string(bus) +
494                                        " address " + std::to_string(ii);
495             std::pair<std::vector<uint8_t>, bool> pair =
496                 readFRUContents(reader, errorMessage);
497             const bool foundHeader = pair.second;
498 
499             if (!foundHeader && !is16BitBool)
500             {
501                 // certain FRU eeproms require bytewise reading.
502                 // otherwise garbage is read. e.g. SuperMicro PWS 920P-SQ
503 
504                 auto readFunc = [is16BitBool, file, ii](off_t offset,
505                                                         size_t length,
506                                                         uint8_t* outbuf) {
507                     return readData(is16BitBool, true, file, ii, offset, length,
508                                     outbuf);
509                 };
510                 FRUReader readerBytewise(std::move(readFunc));
511                 pair = readFRUContents(readerBytewise, errorMessage);
512             }
513 
514             if (pair.first.empty())
515             {
516                 continue;
517             }
518 
519             devices->emplace(ii, pair.first);
520             fruAddresses[bus].insert(ii);
521         }
522         return 1;
523     });
524     std::future_status status =
525         future.wait_for(std::chrono::seconds(busTimeoutSeconds));
526     if (status == std::future_status::timeout)
527     {
528         std::cerr << "Error reading bus " << bus << "\n";
529         if (powerIsOn)
530         {
531             busBlacklist.insert(bus);
532         }
533         close(file);
534         return -1;
535     }
536 
537     close(file);
538     return future.get();
539 }
540 
541 void loadBlacklist(const char* path)
542 {
543     std::ifstream blacklistStream(path);
544     if (!blacklistStream.good())
545     {
546         // File is optional.
547         std::cerr << "Cannot open blacklist file.\n\n";
548         return;
549     }
550 
551     nlohmann::json data = nlohmann::json::parse(blacklistStream, nullptr,
552                                                 false);
553     if (data.is_discarded())
554     {
555         std::cerr << "Illegal blacklist file detected, cannot validate JSON, "
556                      "exiting\n";
557         std::exit(EXIT_FAILURE);
558     }
559 
560     // It's expected to have at least one field, "buses" that is an array of the
561     // buses by integer. Allow for future options to exclude further aspects,
562     // such as specific addresses or ranges.
563     if (data.type() != nlohmann::json::value_t::object)
564     {
565         std::cerr << "Illegal blacklist, expected to read dictionary\n";
566         std::exit(EXIT_FAILURE);
567     }
568 
569     // If buses field is missing, that's fine.
570     if (data.count("buses") == 1)
571     {
572         // Parse the buses array after a little validation.
573         auto buses = data.at("buses");
574         if (buses.type() != nlohmann::json::value_t::array)
575         {
576             // Buses field present but invalid, therefore this is an error.
577             std::cerr << "Invalid contents for blacklist buses field\n";
578             std::exit(EXIT_FAILURE);
579         }
580 
581         // Catch exception here for type mis-match.
582         try
583         {
584             for (const auto& bus : buses)
585             {
586                 busBlacklist.insert(bus.get<size_t>());
587             }
588         }
589         catch (const nlohmann::detail::type_error& e)
590         {
591             // Type mis-match is a critical error.
592             std::cerr << "Invalid bus type: " << e.what() << "\n";
593             std::exit(EXIT_FAILURE);
594         }
595     }
596 }
597 
598 static void findI2CDevices(const std::vector<fs::path>& i2cBuses,
599                            BusMap& busmap, const bool& powerIsOn,
600                            sdbusplus::asio::object_server& objServer)
601 {
602     for (const auto& i2cBus : i2cBuses)
603     {
604         int bus = busStrToInt(i2cBus.string());
605 
606         if (bus < 0)
607         {
608             std::cerr << "Cannot translate " << i2cBus << " to int\n";
609             continue;
610         }
611         if (busBlacklist.find(bus) != busBlacklist.end())
612         {
613             continue; // skip previously failed busses
614         }
615 
616         int rootBus = getRootBus(bus);
617         if (busBlacklist.find(rootBus) != busBlacklist.end())
618         {
619             continue;
620         }
621 
622         auto file = open(i2cBus.c_str(), O_RDWR);
623         if (file < 0)
624         {
625             std::cerr << "unable to open i2c device " << i2cBus.string()
626                       << "\n";
627             continue;
628         }
629         unsigned long funcs = 0;
630 
631         if (ioctl(file, I2C_FUNCS, &funcs) < 0)
632         {
633             std::cerr
634                 << "Error: Could not get the adapter functionality matrix bus "
635                 << bus << "\n";
636             close(file);
637             continue;
638         }
639         if (((funcs & I2C_FUNC_SMBUS_READ_BYTE) == 0U) ||
640             ((I2C_FUNC_SMBUS_READ_I2C_BLOCK) == 0))
641         {
642             std::cerr << "Error: Can't use SMBus Receive Byte command bus "
643                       << bus << "\n";
644             continue;
645         }
646         auto& device = busmap[bus];
647         device = std::make_shared<DeviceMap>();
648 
649         //  i2cdetect by default uses the range 0x03 to 0x77, as
650         //  this is  what we have tested with, use this range. Could be
651         //  changed in future.
652         if (debug)
653         {
654             std::cerr << "Scanning bus " << bus << "\n";
655         }
656 
657         // fd is closed in this function in case the bus locks up
658         getBusFRUs(file, 0x03, 0x77, bus, device, powerIsOn, objServer);
659 
660         if (debug)
661         {
662             std::cerr << "Done scanning bus " << bus << "\n";
663         }
664     }
665 }
666 
667 // this class allows an async response after all i2c devices are discovered
668 struct FindDevicesWithCallback :
669     std::enable_shared_from_this<FindDevicesWithCallback>
670 {
671     FindDevicesWithCallback(const std::vector<fs::path>& i2cBuses,
672                             BusMap& busmap, const bool& powerIsOn,
673                             sdbusplus::asio::object_server& objServer,
674                             std::function<void(void)>&& callback) :
675         _i2cBuses(i2cBuses),
676         _busMap(busmap), _powerIsOn(powerIsOn), _objServer(objServer),
677         _callback(std::move(callback))
678     {}
679     ~FindDevicesWithCallback()
680     {
681         _callback();
682     }
683     void run()
684     {
685         findI2CDevices(_i2cBuses, _busMap, _powerIsOn, _objServer);
686     }
687 
688     const std::vector<fs::path>& _i2cBuses;
689     BusMap& _busMap;
690     const bool& _powerIsOn;
691     sdbusplus::asio::object_server& _objServer;
692     std::function<void(void)> _callback;
693 };
694 
695 void addFruObjectToDbus(
696     std::vector<uint8_t>& device,
697     boost::container::flat_map<
698         std::pair<size_t, size_t>,
699         std::shared_ptr<sdbusplus::asio::dbus_interface>>& dbusInterfaceMap,
700     uint32_t bus, uint32_t address, size_t& unknownBusObjectCount,
701     const bool& powerIsOn, sdbusplus::asio::object_server& objServer,
702     std::shared_ptr<sdbusplus::asio::connection>& systemBus)
703 {
704     boost::container::flat_map<std::string, std::string> formattedFRU;
705 
706     std::optional<std::string> optionalProductName = getProductName(
707         device, formattedFRU, bus, address, unknownBusObjectCount);
708     if (!optionalProductName)
709     {
710         std::cerr << "getProductName failed. product name is empty.\n";
711         return;
712     }
713 
714     std::string productName = "/xyz/openbmc_project/FruDevice/" +
715                               optionalProductName.value();
716 
717     std::optional<int> index = findIndexForFRU(dbusInterfaceMap, productName);
718     if (index.has_value())
719     {
720         productName += "_";
721         productName += std::to_string(++(*index));
722     }
723 
724     std::shared_ptr<sdbusplus::asio::dbus_interface> iface =
725         objServer.add_interface(productName, "xyz.openbmc_project.FruDevice");
726     dbusInterfaceMap[std::pair<size_t, size_t>(bus, address)] = iface;
727 
728     for (auto& property : formattedFRU)
729     {
730         std::regex_replace(property.second.begin(), property.second.begin(),
731                            property.second.end(), nonAsciiRegex, "_");
732         if (property.second.empty() && property.first != "PRODUCT_ASSET_TAG")
733         {
734             continue;
735         }
736         std::string key = std::regex_replace(property.first, nonAsciiRegex,
737                                              "_");
738 
739         if (property.first == "PRODUCT_ASSET_TAG")
740         {
741             std::string propertyName = property.first;
742             iface->register_property(
743                 key, property.second + '\0',
744                 [bus, address, propertyName, &dbusInterfaceMap,
745                  &unknownBusObjectCount, &powerIsOn, &objServer,
746                  &systemBus](const std::string& req, std::string& resp) {
747                 if (strcmp(req.c_str(), resp.c_str()) != 0)
748                 {
749                     // call the method which will update
750                     if (updateFRUProperty(req, bus, address, propertyName,
751                                           dbusInterfaceMap,
752                                           unknownBusObjectCount, powerIsOn,
753                                           objServer, systemBus))
754                     {
755                         resp = req;
756                     }
757                     else
758                     {
759                         throw std::invalid_argument(
760                             "FRU property update failed.");
761                     }
762                 }
763                 return 1;
764                 });
765         }
766         else if (!iface->register_property(key, property.second + '\0'))
767         {
768             std::cerr << "illegal key: " << key << "\n";
769         }
770         if (debug)
771         {
772             std::cout << property.first << ": " << property.second << "\n";
773         }
774     }
775 
776     // baseboard will be 0, 0
777     iface->register_property("BUS", bus);
778     iface->register_property("ADDRESS", address);
779 
780     iface->initialize();
781 }
782 
783 static bool readBaseboardFRU(std::vector<uint8_t>& baseboardFRU)
784 {
785     // try to read baseboard fru from file
786     std::ifstream baseboardFRUFile(baseboardFruLocation, std::ios::binary);
787     if (baseboardFRUFile.good())
788     {
789         baseboardFRUFile.seekg(0, std::ios_base::end);
790         size_t fileSize = static_cast<size_t>(baseboardFRUFile.tellg());
791         baseboardFRU.resize(fileSize);
792         baseboardFRUFile.seekg(0, std::ios_base::beg);
793         // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
794         char* charOffset = reinterpret_cast<char*>(baseboardFRU.data());
795         baseboardFRUFile.read(charOffset, fileSize);
796     }
797     else
798     {
799         return false;
800     }
801     return true;
802 }
803 
804 bool writeFRU(uint8_t bus, uint8_t address, const std::vector<uint8_t>& fru)
805 {
806     boost::container::flat_map<std::string, std::string> tmp;
807     if (fru.size() > maxFruSize)
808     {
809         std::cerr << "Invalid fru.size() during writeFRU\n";
810         return false;
811     }
812     // verify legal fru by running it through fru parsing logic
813     if (formatIPMIFRU(fru, tmp) != resCodes::resOK)
814     {
815         std::cerr << "Invalid fru format during writeFRU\n";
816         return false;
817     }
818     // baseboard fru
819     if (bus == 0 && address == 0)
820     {
821         std::ofstream file(baseboardFruLocation, std::ios_base::binary);
822         if (!file.good())
823         {
824             std::cerr << "Error opening file " << baseboardFruLocation << "\n";
825             throw DBusInternalError();
826             return false;
827         }
828         // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
829         const char* charOffset = reinterpret_cast<const char*>(fru.data());
830         file.write(charOffset, fru.size());
831         return file.good();
832     }
833 
834     if (hasEepromFile(bus, address))
835     {
836         auto path = getEepromPath(bus, address);
837         int eeprom = open(path.c_str(), O_RDWR | O_CLOEXEC);
838         if (eeprom < 0)
839         {
840             std::cerr << "unable to open i2c device " << path << "\n";
841             throw DBusInternalError();
842             return false;
843         }
844 
845         ssize_t writtenBytes = write(eeprom, fru.data(), fru.size());
846         if (writtenBytes < 0)
847         {
848             std::cerr << "unable to write to i2c device " << path << "\n";
849             close(eeprom);
850             throw DBusInternalError();
851             return false;
852         }
853 
854         close(eeprom);
855         return true;
856     }
857 
858     std::string i2cBus = "/dev/i2c-" + std::to_string(bus);
859 
860     int file = open(i2cBus.c_str(), O_RDWR | O_CLOEXEC);
861     if (file < 0)
862     {
863         std::cerr << "unable to open i2c device " << i2cBus << "\n";
864         throw DBusInternalError();
865         return false;
866     }
867     if (ioctl(file, I2C_SLAVE_FORCE, address) < 0)
868     {
869         std::cerr << "unable to set device address\n";
870         close(file);
871         throw DBusInternalError();
872         return false;
873     }
874 
875     constexpr const size_t retryMax = 2;
876     uint16_t index = 0;
877     size_t retries = retryMax;
878     while (index < fru.size())
879     {
880         if (((index != 0U) && ((index % (maxEepromPageIndex + 1)) == 0)) &&
881             (retries == retryMax))
882         {
883             // The 4K EEPROM only uses the A2 and A1 device address bits
884             // with the third bit being a memory page address bit.
885             if (ioctl(file, I2C_SLAVE_FORCE, ++address) < 0)
886             {
887                 std::cerr << "unable to set device address\n";
888                 close(file);
889                 throw DBusInternalError();
890                 return false;
891             }
892         }
893 
894         if (i2c_smbus_write_byte_data(file, static_cast<uint8_t>(index),
895                                       fru[index]) < 0)
896         {
897             if ((retries--) == 0U)
898             {
899                 std::cerr << "error writing fru: " << strerror(errno) << "\n";
900                 close(file);
901                 throw DBusInternalError();
902                 return false;
903             }
904         }
905         else
906         {
907             retries = retryMax;
908             index++;
909         }
910         // most eeproms require 5-10ms between writes
911         std::this_thread::sleep_for(std::chrono::milliseconds(10));
912     }
913     close(file);
914     return true;
915 }
916 
917 void rescanOneBus(
918     BusMap& busmap, uint16_t busNum,
919     boost::container::flat_map<
920         std::pair<size_t, size_t>,
921         std::shared_ptr<sdbusplus::asio::dbus_interface>>& dbusInterfaceMap,
922     bool dbusCall, size_t& unknownBusObjectCount, const bool& powerIsOn,
923     sdbusplus::asio::object_server& objServer,
924     std::shared_ptr<sdbusplus::asio::connection>& systemBus)
925 {
926     for (auto& [pair, interface] : foundDevices)
927     {
928         if (pair.first == static_cast<size_t>(busNum))
929         {
930             objServer.remove_interface(interface);
931             foundDevices.erase(pair);
932         }
933     }
934 
935     fs::path busPath = fs::path("/dev/i2c-" + std::to_string(busNum));
936     if (!fs::exists(busPath))
937     {
938         if (dbusCall)
939         {
940             std::cerr << "Unable to access i2c bus " << static_cast<int>(busNum)
941                       << "\n";
942             throw std::invalid_argument("Invalid Bus.");
943         }
944         return;
945     }
946 
947     std::vector<fs::path> i2cBuses;
948     i2cBuses.emplace_back(busPath);
949 
950     auto scan = std::make_shared<FindDevicesWithCallback>(
951         i2cBuses, busmap, powerIsOn, objServer,
952         [busNum, &busmap, &dbusInterfaceMap, &unknownBusObjectCount, &powerIsOn,
953          &objServer, &systemBus]() {
954         for (auto& busIface : dbusInterfaceMap)
955         {
956             if (busIface.first.first == static_cast<size_t>(busNum))
957             {
958                 objServer.remove_interface(busIface.second);
959             }
960         }
961         auto found = busmap.find(busNum);
962         if (found == busmap.end() || found->second == nullptr)
963         {
964             return;
965         }
966         for (auto& device : *(found->second))
967         {
968             addFruObjectToDbus(device.second, dbusInterfaceMap,
969                                static_cast<uint32_t>(busNum), device.first,
970                                unknownBusObjectCount, powerIsOn, objServer,
971                                systemBus);
972         }
973         });
974     scan->run();
975 }
976 
977 void rescanBusses(
978     BusMap& busmap,
979     boost::container::flat_map<
980         std::pair<size_t, size_t>,
981         std::shared_ptr<sdbusplus::asio::dbus_interface>>& dbusInterfaceMap,
982     size_t& unknownBusObjectCount, const bool& powerIsOn,
983     sdbusplus::asio::object_server& objServer,
984     std::shared_ptr<sdbusplus::asio::connection>& systemBus)
985 {
986     static boost::asio::steady_timer timer(io);
987     timer.expires_from_now(std::chrono::seconds(1));
988 
989     // setup an async wait in case we get flooded with requests
990     timer.async_wait([&](const boost::system::error_code& ec) {
991         if (ec == boost::asio::error::operation_aborted)
992         {
993             return;
994         }
995 
996         if (ec)
997         {
998             std::cerr << "Error in timer: " << ec.message() << "\n";
999             return;
1000         }
1001 
1002         auto devDir = fs::path("/dev/");
1003         std::vector<fs::path> i2cBuses;
1004 
1005         boost::container::flat_map<size_t, fs::path> busPaths;
1006         if (!getI2cDevicePaths(devDir, busPaths))
1007         {
1008             std::cerr << "unable to find i2c devices\n";
1009             return;
1010         }
1011 
1012         for (const auto& busPath : busPaths)
1013         {
1014             i2cBuses.emplace_back(busPath.second);
1015         }
1016 
1017         busmap.clear();
1018         for (auto& [pair, interface] : foundDevices)
1019         {
1020             objServer.remove_interface(interface);
1021         }
1022         foundDevices.clear();
1023 
1024         auto scan = std::make_shared<FindDevicesWithCallback>(
1025             i2cBuses, busmap, powerIsOn, objServer, [&]() {
1026                 for (auto& busIface : dbusInterfaceMap)
1027                 {
1028                     objServer.remove_interface(busIface.second);
1029                 }
1030 
1031                 dbusInterfaceMap.clear();
1032                 unknownBusObjectCount = 0;
1033 
1034                 // todo, get this from a more sensable place
1035                 std::vector<uint8_t> baseboardFRU;
1036                 if (readBaseboardFRU(baseboardFRU))
1037                 {
1038                     // If no device on i2c bus 0, the insertion will happen.
1039                     auto bus0 =
1040                         busmap.try_emplace(0, std::make_shared<DeviceMap>());
1041                     bus0.first->second->emplace(0, baseboardFRU);
1042                 }
1043                 for (auto& devicemap : busmap)
1044                 {
1045                     for (auto& device : *devicemap.second)
1046                     {
1047                         addFruObjectToDbus(device.second, dbusInterfaceMap,
1048                                            devicemap.first, device.first,
1049                                            unknownBusObjectCount, powerIsOn,
1050                                            objServer, systemBus);
1051                     }
1052                 }
1053             });
1054         scan->run();
1055     });
1056 }
1057 
1058 // Details with example of Asset Tag Update
1059 // To find location of Product Info Area asset tag as per FRU specification
1060 // 1. Find product Info area starting offset (*8 - as header will be in
1061 // multiple of 8 bytes).
1062 // 2. Skip 3 bytes of product info area (like format version, area length,
1063 // and language code).
1064 // 3. Traverse manufacturer name, product name, product version, & product
1065 // serial number, by reading type/length code to reach the Asset Tag.
1066 // 4. Update the Asset Tag, reposition the product Info area in multiple of
1067 // 8 bytes. Update the Product area length and checksum.
1068 
1069 bool updateFRUProperty(
1070     const std::string& updatePropertyReq, uint32_t bus, uint32_t address,
1071     const std::string& propertyName,
1072     boost::container::flat_map<
1073         std::pair<size_t, size_t>,
1074         std::shared_ptr<sdbusplus::asio::dbus_interface>>& dbusInterfaceMap,
1075     size_t& unknownBusObjectCount, const bool& powerIsOn,
1076     sdbusplus::asio::object_server& objServer,
1077     std::shared_ptr<sdbusplus::asio::connection>& systemBus)
1078 {
1079     size_t updatePropertyReqLen = updatePropertyReq.length();
1080     if (updatePropertyReqLen == 1 || updatePropertyReqLen > 63)
1081     {
1082         std::cerr
1083             << "FRU field data cannot be of 1 char or more than 63 chars. "
1084                "Invalid Length "
1085             << updatePropertyReqLen << "\n";
1086         return false;
1087     }
1088 
1089     std::vector<uint8_t> fruData;
1090 
1091     if (!getFruData(fruData, bus, address))
1092     {
1093         std::cerr << "Failure getting FRU Data \n";
1094         return false;
1095     }
1096 
1097     struct FruArea fruAreaParams
1098     {};
1099 
1100     if (!findFruAreaLocationAndField(fruData, propertyName, fruAreaParams))
1101     {
1102         std::cerr << "findFruAreaLocationAndField failed \n";
1103         return false;
1104     }
1105 
1106     std::vector<uint8_t> restFRUAreaFieldsData;
1107     if (!copyRestFRUArea(fruData, propertyName, fruAreaParams,
1108                          restFRUAreaFieldsData))
1109     {
1110         std::cerr << "copyRestFRUArea failed \n";
1111         return false;
1112     }
1113 
1114     // Push post update fru areas if any
1115     unsigned int nextFRUAreaLoc = 0;
1116     for (fruAreas nextFRUArea = fruAreas::fruAreaInternal;
1117          nextFRUArea <= fruAreas::fruAreaMultirecord; ++nextFRUArea)
1118     {
1119         unsigned int fruAreaLoc =
1120             fruData[getHeaderAreaFieldOffset(nextFRUArea)] * fruBlockSize;
1121         if ((fruAreaLoc > fruAreaParams.restFieldsEnd) &&
1122             ((nextFRUAreaLoc == 0) || (fruAreaLoc < nextFRUAreaLoc)))
1123         {
1124             nextFRUAreaLoc = fruAreaLoc;
1125         }
1126     }
1127     std::vector<uint8_t> restFRUAreasData;
1128     if (nextFRUAreaLoc != 0U)
1129     {
1130         std::copy_n(fruData.begin() + nextFRUAreaLoc,
1131                     fruData.size() - nextFRUAreaLoc,
1132                     std::back_inserter(restFRUAreasData));
1133     }
1134 
1135     // check FRU area size
1136     size_t fruAreaDataSize =
1137         ((fruAreaParams.updateFieldLoc - fruAreaParams.start + 1) +
1138          restFRUAreaFieldsData.size());
1139     size_t fruAreaAvailableSize = fruAreaParams.size - fruAreaDataSize;
1140     if ((updatePropertyReqLen + 1) > fruAreaAvailableSize)
1141     {
1142 #ifdef ENABLE_FRU_AREA_RESIZE
1143         size_t newFRUAreaSize = fruAreaDataSize + updatePropertyReqLen + 1;
1144         // round size to 8-byte blocks
1145         newFRUAreaSize = ((newFRUAreaSize - 1) / fruBlockSize + 1) *
1146                          fruBlockSize;
1147         size_t newFRUDataSize = fruData.size() + newFRUAreaSize -
1148                                 fruAreaParams.size;
1149         fruData.resize(newFRUDataSize);
1150         fruAreaParams.size = newFRUAreaSize;
1151         fruAreaParams.end = fruAreaParams.start + fruAreaParams.size;
1152 #else
1153         std::cerr << "FRU field length: " << updatePropertyReqLen + 1
1154                   << " should not be greater than available FRU area size: "
1155                   << fruAreaAvailableSize << "\n";
1156         return false;
1157 #endif // ENABLE_FRU_AREA_RESIZE
1158     }
1159 
1160     // write new requested property field length and data
1161     constexpr uint8_t newTypeLenMask = 0xC0;
1162     fruData[fruAreaParams.updateFieldLoc] =
1163         static_cast<uint8_t>(updatePropertyReqLen | newTypeLenMask);
1164     fruAreaParams.updateFieldLoc++;
1165     std::copy(updatePropertyReq.begin(), updatePropertyReq.end(),
1166               fruData.begin() + fruAreaParams.updateFieldLoc);
1167 
1168     // Copy remaining data to main fru area - post updated fru field vector
1169     fruAreaParams.restFieldsLoc = fruAreaParams.updateFieldLoc +
1170                                   updatePropertyReqLen;
1171     size_t fruAreaDataEnd = fruAreaParams.restFieldsLoc +
1172                             restFRUAreaFieldsData.size();
1173 
1174     std::copy(restFRUAreaFieldsData.begin(), restFRUAreaFieldsData.end(),
1175               fruData.begin() + fruAreaParams.restFieldsLoc);
1176 
1177     // Update final fru with new fru area length and checksum
1178     unsigned int nextFRUAreaNewLoc = updateFRUAreaLenAndChecksum(
1179         fruData, fruAreaParams.start, fruAreaDataEnd, fruAreaParams.end);
1180 
1181 #ifdef ENABLE_FRU_AREA_RESIZE
1182     ++nextFRUAreaNewLoc;
1183     ssize_t nextFRUAreaOffsetDiff = (nextFRUAreaNewLoc - nextFRUAreaLoc) /
1184                                     fruBlockSize;
1185     // Append rest FRU Areas if size changed and there were other sections after
1186     // updated one
1187     if (nextFRUAreaOffsetDiff && nextFRUAreaLoc)
1188     {
1189         std::copy(restFRUAreasData.begin(), restFRUAreasData.end(),
1190                   fruData.begin() + nextFRUAreaNewLoc);
1191         // Update Common Header
1192         for (fruAreas nextFRUArea = fruAreas::fruAreaInternal;
1193              nextFRUArea <= fruAreas::fruAreaMultirecord; ++nextFRUArea)
1194         {
1195             unsigned int fruAreaOffsetField =
1196                 getHeaderAreaFieldOffset(nextFRUArea);
1197             size_t curFRUAreaOffset = fruData[fruAreaOffsetField];
1198             if (curFRUAreaOffset > fruAreaParams.end)
1199             {
1200                 fruData[fruAreaOffsetField] = static_cast<int8_t>(
1201                     curFRUAreaOffset + nextFRUAreaOffsetDiff);
1202             }
1203         }
1204         // Calculate new checksum
1205         std::vector<uint8_t> headerFRUData;
1206         std::copy_n(fruData.begin(), 7, std::back_inserter(headerFRUData));
1207         size_t checksumVal = calculateChecksum(headerFRUData);
1208         fruData[7] = static_cast<uint8_t>(checksumVal);
1209         // fill zeros if FRU Area size decreased
1210         if (nextFRUAreaOffsetDiff < 0)
1211         {
1212             std::fill(fruData.begin() + nextFRUAreaNewLoc +
1213                           restFRUAreasData.size(),
1214                       fruData.end(), 0);
1215         }
1216     }
1217 #else
1218     // this is to avoid "unused variable" warning
1219     (void)nextFRUAreaNewLoc;
1220 #endif // ENABLE_FRU_AREA_RESIZE
1221     if (fruData.empty())
1222     {
1223         return false;
1224     }
1225 
1226     if (!writeFRU(static_cast<uint8_t>(bus), static_cast<uint8_t>(address),
1227                   fruData))
1228     {
1229         return false;
1230     }
1231 
1232     // Rescan the bus so that GetRawFru dbus-call fetches updated values
1233     rescanBusses(busMap, dbusInterfaceMap, unknownBusObjectCount, powerIsOn,
1234                  objServer, systemBus);
1235     return true;
1236 }
1237 
1238 int main()
1239 {
1240     auto systemBus = std::make_shared<sdbusplus::asio::connection>(io);
1241     sdbusplus::asio::object_server objServer(systemBus);
1242 
1243     static size_t unknownBusObjectCount = 0;
1244     static bool powerIsOn = false;
1245     auto devDir = fs::path("/dev/");
1246     auto matchString = std::string(R"(i2c-\d+$)");
1247     std::vector<fs::path> i2cBuses;
1248 
1249     if (!findFiles(devDir, matchString, i2cBuses))
1250     {
1251         std::cerr << "unable to find i2c devices\n";
1252         return 1;
1253     }
1254 
1255     // check for and load blacklist with initial buses.
1256     loadBlacklist(blacklistPath);
1257 
1258     systemBus->request_name("xyz.openbmc_project.FruDevice");
1259 
1260     // this is a map with keys of pair(bus number, address) and values of
1261     // the object on dbus
1262     boost::container::flat_map<std::pair<size_t, size_t>,
1263                                std::shared_ptr<sdbusplus::asio::dbus_interface>>
1264         dbusInterfaceMap;
1265 
1266     std::shared_ptr<sdbusplus::asio::dbus_interface> iface =
1267         objServer.add_interface("/xyz/openbmc_project/FruDevice",
1268                                 "xyz.openbmc_project.FruDeviceManager");
1269 
1270     iface->register_method("ReScan", [&]() {
1271         rescanBusses(busMap, dbusInterfaceMap, unknownBusObjectCount, powerIsOn,
1272                      objServer, systemBus);
1273     });
1274 
1275     iface->register_method("ReScanBus", [&](uint16_t bus) {
1276         rescanOneBus(busMap, bus, dbusInterfaceMap, true, unknownBusObjectCount,
1277                      powerIsOn, objServer, systemBus);
1278     });
1279 
1280     iface->register_method("GetRawFru", getFRUInfo);
1281 
1282     iface->register_method("WriteFru",
1283                            [&](const uint16_t bus, const uint8_t address,
1284                                const std::vector<uint8_t>& data) {
1285         if (!writeFRU(bus, address, data))
1286         {
1287             throw std::invalid_argument("Invalid Arguments.");
1288             return;
1289         }
1290         // schedule rescan on success
1291         rescanBusses(busMap, dbusInterfaceMap, unknownBusObjectCount, powerIsOn,
1292                      objServer, systemBus);
1293     });
1294     iface->initialize();
1295 
1296     std::function<void(sdbusplus::message_t & message)> eventHandler =
1297         [&](sdbusplus::message_t& message) {
1298         std::string objectName;
1299         boost::container::flat_map<
1300             std::string,
1301             std::variant<std::string, bool, int64_t, uint64_t, double>>
1302             values;
1303         message.read(objectName, values);
1304         auto findState = values.find("CurrentHostState");
1305         if (findState != values.end())
1306         {
1307             if (std::get<std::string>(findState->second) ==
1308                 "xyz.openbmc_project.State.Host.HostState.Running")
1309             {
1310                 powerIsOn = true;
1311             }
1312         }
1313 
1314         if (powerIsOn)
1315         {
1316             rescanBusses(busMap, dbusInterfaceMap, unknownBusObjectCount,
1317                          powerIsOn, objServer, systemBus);
1318         }
1319     };
1320 
1321     sdbusplus::bus::match_t powerMatch = sdbusplus::bus::match_t(
1322         static_cast<sdbusplus::bus_t&>(*systemBus),
1323         "type='signal',interface='org.freedesktop.DBus.Properties',path='/xyz/"
1324         "openbmc_project/state/"
1325         "host0',arg0='xyz.openbmc_project.State.Host'",
1326         eventHandler);
1327 
1328     int fd = inotify_init();
1329     inotify_add_watch(fd, i2CDevLocation, IN_CREATE | IN_MOVED_TO | IN_DELETE);
1330     std::array<char, 4096> readBuffer{};
1331     // monitor for new i2c devices
1332     boost::asio::posix::stream_descriptor dirWatch(io, fd);
1333     std::function<void(const boost::system::error_code, std::size_t)>
1334         watchI2cBusses =
1335             [&](const boost::system::error_code& ec,
1336                 std::size_t bytesTransferred) {
1337         if (ec)
1338         {
1339             std::cout << "Callback Error " << ec << "\n";
1340             return;
1341         }
1342         size_t index = 0;
1343         while ((index + sizeof(inotify_event)) <= bytesTransferred)
1344         {
1345             const char* p = &readBuffer[index];
1346             // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
1347             const auto* iEvent = reinterpret_cast<const inotify_event*>(p);
1348             switch (iEvent->mask)
1349             {
1350                 case IN_CREATE:
1351                 case IN_MOVED_TO:
1352                 case IN_DELETE:
1353                     std::string_view name(&iEvent->name[0], iEvent->len);
1354                     if (boost::starts_with(name, "i2c"))
1355                     {
1356                         int bus = busStrToInt(name);
1357                         if (bus < 0)
1358                         {
1359                             std::cerr << "Could not parse bus " << name << "\n";
1360                             continue;
1361                         }
1362                         int rootBus = getRootBus(bus);
1363                         if (rootBus >= 0)
1364                         {
1365                             rescanOneBus(busMap, static_cast<uint16_t>(rootBus),
1366                                          dbusInterfaceMap, false,
1367                                          unknownBusObjectCount, powerIsOn,
1368                                          objServer, systemBus);
1369                         }
1370                         rescanOneBus(busMap, static_cast<uint16_t>(bus),
1371                                      dbusInterfaceMap, false,
1372                                      unknownBusObjectCount, powerIsOn,
1373                                      objServer, systemBus);
1374                     }
1375             }
1376             index += sizeof(inotify_event) + iEvent->len;
1377         }
1378 
1379         dirWatch.async_read_some(boost::asio::buffer(readBuffer),
1380                                  watchI2cBusses);
1381     };
1382 
1383     dirWatch.async_read_some(boost::asio::buffer(readBuffer), watchI2cBusses);
1384     // run the initial scan
1385     rescanBusses(busMap, dbusInterfaceMap, unknownBusObjectCount, powerIsOn,
1386                  objServer, systemBus);
1387 
1388     io.run();
1389     return 0;
1390 }
1391