|  | #include "NVMeBasicContext.hpp" | 
|  |  | 
|  | #include "NVMeContext.hpp" | 
|  | #include "NVMeSensor.hpp" | 
|  |  | 
|  | #include <endian.h> | 
|  | #include <sys/ioctl.h> | 
|  | #include <unistd.h> | 
|  |  | 
|  | #include <FileHandle.hpp> | 
|  | #include <boost/asio/buffer.hpp> | 
|  | #include <boost/asio/error.hpp> | 
|  | #include <boost/asio/io_context.hpp> | 
|  | #include <boost/asio/read.hpp> | 
|  | #include <boost/asio/streambuf.hpp> | 
|  | #include <boost/asio/write.hpp> | 
|  | #include <phosphor-logging/lg2.hpp> | 
|  | #include <phosphor-logging/lg2/flags.hpp> | 
|  |  | 
|  | #include <array> | 
|  | #include <cerrno> | 
|  | #include <chrono> | 
|  | #include <cmath> | 
|  | #include <cstdint> | 
|  | #include <cstdio> | 
|  | #include <cstring> | 
|  | #include <filesystem> | 
|  | #include <iostream> | 
|  | #include <iterator> | 
|  | #include <limits> | 
|  | #include <memory> | 
|  | #include <stdexcept> | 
|  | #include <string> | 
|  | #include <system_error> | 
|  | #include <thread> | 
|  | #include <utility> | 
|  | #include <vector> | 
|  |  | 
|  | extern "C" | 
|  | { | 
|  | #include <i2c/smbus.h> | 
|  | #include <linux/i2c-dev.h> | 
|  | } | 
|  |  | 
|  | /* | 
|  | * NVMe-MI Basic Management Command | 
|  | * | 
|  | * https://nvmexpress.org/wp-content/uploads/NVMe_Management_-_Technical_Note_on_Basic_Management_Command.pdf | 
|  | */ | 
|  |  | 
|  | static std::shared_ptr<std::array<uint8_t, 6>> encodeBasicQuery( | 
|  | int bus, uint8_t device, uint8_t offset) | 
|  | { | 
|  | if (bus < 0) | 
|  | { | 
|  | throw std::domain_error("Invalid bus argument"); | 
|  | } | 
|  |  | 
|  | /* bus + address + command */ | 
|  | uint32_t busle = htole32(static_cast<uint32_t>(bus)); | 
|  | auto command = | 
|  | std::make_shared<std::array<uint8_t, sizeof(busle) + 1 + 1>>(); | 
|  | memcpy(command->data(), &busle, sizeof(busle)); | 
|  | (*command)[sizeof(busle) + 0] = device; | 
|  | (*command)[sizeof(busle) + 1] = offset; | 
|  |  | 
|  | return command; | 
|  | } | 
|  |  | 
|  | static void decodeBasicQuery(const std::array<uint8_t, 6>& req, int& bus, | 
|  | uint8_t& device, uint8_t& offset) | 
|  | { | 
|  | uint32_t busle = 0; | 
|  |  | 
|  | memcpy(&busle, req.data(), sizeof(busle)); | 
|  | bus = le32toh(busle); | 
|  | device = req[sizeof(busle) + 0]; | 
|  | offset = req[sizeof(busle) + 1]; | 
|  | } | 
|  |  | 
|  | static void execBasicQuery(int bus, uint8_t addr, uint8_t cmd, | 
|  | std::vector<uint8_t>& resp) | 
|  | { | 
|  | int32_t size = 0; | 
|  | std::filesystem::path devpath = "/dev/i2c-" + std::to_string(bus); | 
|  |  | 
|  | try | 
|  | { | 
|  | FileHandle fileHandle(devpath); | 
|  |  | 
|  | /* Select the target device */ | 
|  | // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) | 
|  | if (::ioctl(fileHandle.handle(), I2C_SLAVE, addr) == -1) | 
|  | { | 
|  | lg2::error( | 
|  | "Failed to configure device address '{ADDR}' for bus '{BUS}': ERRNO", | 
|  | "ADDR", lg2::hex, addr, "BUS", bus, "ERRNO", lg2::hex, errno); | 
|  | resp.resize(0); | 
|  | return; | 
|  | } | 
|  |  | 
|  | resp.resize(UINT8_MAX + 1); | 
|  |  | 
|  | /* Issue the NVMe MI basic command */ | 
|  | size = i2c_smbus_read_block_data(fileHandle.handle(), cmd, resp.data()); | 
|  | if (size < 0) | 
|  | { | 
|  | lg2::error( | 
|  | "Failed to read block data from device '{ADDR}' on bus '{BUS}': ERRNO", | 
|  | "ADDR", lg2::hex, addr, "BUS", bus, "ERRNO", lg2::hex, errno); | 
|  | resp.resize(0); | 
|  | } | 
|  | else if (size > UINT8_MAX + 1) | 
|  | { | 
|  | lg2::error( | 
|  | "Unexpected message length from device '{ADDR}' on bus '{BUS}': '{SIZE}' ({MAX})", | 
|  | "ADDR", lg2::hex, addr, "BUS", bus, "SIZE", size, "MAX", | 
|  | UINT8_MAX); | 
|  | resp.resize(0); | 
|  | } | 
|  | else | 
|  | { | 
|  | resp.resize(size); | 
|  | } | 
|  | } | 
|  | catch (const std::out_of_range& e) | 
|  | { | 
|  | lg2::error("Failed to create file handle for bus '{BUS}': '{ERR}'", | 
|  | "BUS", bus, "ERR", e); | 
|  | resp.resize(0); | 
|  | } | 
|  | } | 
|  |  | 
|  | static ssize_t processBasicQueryStream(FileHandle& in, FileHandle& out) | 
|  | { | 
|  | std::vector<uint8_t> resp{}; | 
|  | ssize_t rc = 0; | 
|  |  | 
|  | while (true) | 
|  | { | 
|  | uint8_t device = 0; | 
|  | uint8_t offset = 0; | 
|  | uint8_t len = 0; | 
|  | int bus = 0; | 
|  |  | 
|  | /* bus + address + command */ | 
|  | std::array<uint8_t, sizeof(uint32_t) + 1 + 1> req{}; | 
|  |  | 
|  | /* Read the command parameters */ | 
|  | ssize_t rc = ::read(in.handle(), req.data(), req.size()); | 
|  | if (rc != static_cast<ssize_t>(req.size())) | 
|  | { | 
|  | lg2::error( | 
|  | "Failed to read request from in descriptor '{ERROR_MESSAGE}'", | 
|  | "ERROR_MESSAGE", strerror(errno)); | 
|  | if (rc != 0) | 
|  | { | 
|  | return -errno; | 
|  | } | 
|  | return -EIO; | 
|  | } | 
|  |  | 
|  | decodeBasicQuery(req, bus, device, offset); | 
|  |  | 
|  | /* Execute the query */ | 
|  | execBasicQuery(bus, device, offset, resp); | 
|  |  | 
|  | /* Write out the response length */ | 
|  | len = resp.size(); | 
|  | rc = ::write(out.handle(), &len, sizeof(len)); | 
|  | if (rc != sizeof(len)) | 
|  | { | 
|  | lg2::error( | 
|  | "Failed to write block ({LEN}) length to out descriptor: '{ERRNO}'", | 
|  | "LEN", len, "ERRNO", strerror(static_cast<int>(-rc))); | 
|  | if (rc != 0) | 
|  | { | 
|  | return -errno; | 
|  | } | 
|  | return -EIO; | 
|  | } | 
|  |  | 
|  | /* Write out the response data */ | 
|  | std::vector<uint8_t>::iterator cursor = resp.begin(); | 
|  | while (cursor != resp.end()) | 
|  | { | 
|  | size_t lenRemaining = std::distance(cursor, resp.end()); | 
|  | ssize_t egress = ::write(out.handle(), &(*cursor), lenRemaining); | 
|  | if (egress == -1) | 
|  | { | 
|  | lg2::error( | 
|  | "Failed to write block data of length '{LEN}' to out pipe: '{ERROR_MESSAGE}'", | 
|  | "LEN", lenRemaining, "ERROR_MESSAGE", strerror(errno)); | 
|  | if (rc != 0) | 
|  | { | 
|  | return -errno; | 
|  | } | 
|  | return -EIO; | 
|  | } | 
|  |  | 
|  | cursor += egress; | 
|  | } | 
|  | } | 
|  |  | 
|  | return rc; | 
|  | } | 
|  |  | 
|  | /* Throws std::error_code on failure */ | 
|  | /* FIXME: Probably shouldn't do fallible stuff in a constructor */ | 
|  | NVMeBasicContext::NVMeBasicContext(boost::asio::io_context& io, int rootBus) : | 
|  | NVMeContext::NVMeContext(io, rootBus), io(io), reqStream(io), respStream(io) | 
|  | { | 
|  | std::array<int, 2> responsePipe{}; | 
|  | std::array<int, 2> requestPipe{}; | 
|  |  | 
|  | /* Set up inter-thread communication */ | 
|  | if (::pipe(requestPipe.data()) == -1) | 
|  | { | 
|  | lg2::error("Failed to create request pipe: '{ERROR}'", "ERROR", | 
|  | strerror(errno)); | 
|  | throw std::error_code(errno, std::system_category()); | 
|  | } | 
|  |  | 
|  | if (::pipe(responsePipe.data()) == -1) | 
|  | { | 
|  | lg2::error("Failed to create response pipe: '{ERROR}'", "ERROR", | 
|  | strerror(errno)); | 
|  |  | 
|  | if (::close(requestPipe[0]) == -1) | 
|  | { | 
|  | lg2::error("Failed to close write fd of request pipe '{ERROR}'", | 
|  | "ERROR", strerror(errno)); | 
|  | } | 
|  |  | 
|  | if (::close(requestPipe[1]) == -1) | 
|  | { | 
|  | lg2::error("Failed to close read fd of request pipe '{ERROR}'", | 
|  | "ERROR", strerror(errno)); | 
|  | } | 
|  |  | 
|  | throw std::error_code(errno, std::system_category()); | 
|  | } | 
|  |  | 
|  | reqStream.assign(requestPipe[1]); | 
|  | FileHandle streamIn(requestPipe[0]); | 
|  | FileHandle streamOut(responsePipe[1]); | 
|  | respStream.assign(responsePipe[0]); | 
|  |  | 
|  | thread = std::jthread([streamIn{std::move(streamIn)}, | 
|  | streamOut{std::move(streamOut)}]() mutable { | 
|  | ssize_t rc = processBasicQueryStream(streamIn, streamOut); | 
|  |  | 
|  | if (rc < 0) | 
|  | { | 
|  | lg2::error("Failure while processing query stream: '{ERROR}'", | 
|  | "ERROR", strerror(static_cast<int>(-rc))); | 
|  | } | 
|  |  | 
|  | lg2::error("Terminating basic query thread"); | 
|  | }); | 
|  | } | 
|  |  | 
|  | void NVMeBasicContext::readAndProcessNVMeSensor() | 
|  | { | 
|  | if (pollCursor == sensors.end()) | 
|  | { | 
|  | this->pollNVMeDevices(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | std::shared_ptr<NVMeSensor> sensor = *pollCursor++; | 
|  |  | 
|  | if (!sensor->readingStateGood()) | 
|  | { | 
|  | sensor->markAvailable(false); | 
|  | sensor->updateValue(std::numeric_limits<double>::quiet_NaN()); | 
|  | readAndProcessNVMeSensor(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | /* Potentially defer sampling the sensor sensor if it is in error */ | 
|  | if (!sensor->sample()) | 
|  | { | 
|  | readAndProcessNVMeSensor(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | auto command = encodeBasicQuery(sensor->bus, sensor->address, 0x00); | 
|  |  | 
|  | /* Issue the request */ | 
|  | boost::asio::async_write( | 
|  | reqStream, boost::asio::buffer(command->data(), command->size()), | 
|  | [command](boost::system::error_code ec, std::size_t) { | 
|  | if (ec) | 
|  | { | 
|  | lg2::error("Got error writing basic query: '{ERROR_MESSAGE}'", | 
|  | "ERROR_MESSAGE", ec.message()); | 
|  | } | 
|  | }); | 
|  |  | 
|  | auto response = std::make_shared<boost::asio::streambuf>(); | 
|  | response->prepare(1); | 
|  |  | 
|  | /* Gather the response and dispatch for parsing */ | 
|  | boost::asio::async_read( | 
|  | respStream, *response, | 
|  | [response](const boost::system::error_code& ec, std::size_t n) { | 
|  | if (ec) | 
|  | { | 
|  | lg2::error( | 
|  | "Got error completing basic query: '{ERROR_MESSAGE}'", | 
|  | "ERROR_MESSAGE", ec.message()); | 
|  | return static_cast<std::size_t>(0); | 
|  | } | 
|  |  | 
|  | if (n == 0) | 
|  | { | 
|  | return static_cast<std::size_t>(1); | 
|  | } | 
|  |  | 
|  | std::istream is(response.get()); | 
|  | size_t len = static_cast<std::size_t>(is.peek()); | 
|  |  | 
|  | if (n > len + 1) | 
|  | { | 
|  | lg2::error( | 
|  | "Query stream has become unsynchronised: n: {N}, len: {LEN}", | 
|  | "N", n, "LEN", len); | 
|  | return static_cast<std::size_t>(0); | 
|  | } | 
|  |  | 
|  | if (n == len + 1) | 
|  | { | 
|  | return static_cast<std::size_t>(0); | 
|  | } | 
|  |  | 
|  | if (n > 1) | 
|  | { | 
|  | return len + 1 - n; | 
|  | } | 
|  |  | 
|  | response->prepare(len); | 
|  | return len; | 
|  | }, | 
|  | [weakSelf{weak_from_this()}, sensor, response]( | 
|  | const boost::system::error_code& ec, std::size_t length) mutable { | 
|  | if (ec) | 
|  | { | 
|  | lg2::error("Got error reading basic query: '{ERROR_MESSAGE}'", | 
|  | "ERROR_MESSAGE", ec.message()); | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (length == 0) | 
|  | { | 
|  | lg2::error("Invalid message length: '{LEN}'", "LEN", length); | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (auto self = weakSelf.lock()) | 
|  | { | 
|  | /* Deserialise the response */ | 
|  | response->consume(1); /* Drop the length byte */ | 
|  | std::istream is(response.get()); | 
|  | std::vector<char> data(response->size()); | 
|  | is.read(data.data(), response->size()); | 
|  |  | 
|  | /* Update the sensor */ | 
|  | self->processResponse(sensor, data.data(), data.size()); | 
|  |  | 
|  | /* Enqueue processing of the next sensor */ | 
|  | self->readAndProcessNVMeSensor(); | 
|  | } | 
|  | }); | 
|  | } | 
|  |  | 
|  | void NVMeBasicContext::pollNVMeDevices() | 
|  | { | 
|  | pollCursor = sensors.begin(); | 
|  |  | 
|  | scanTimer.expires_after(std::chrono::seconds(1)); | 
|  | scanTimer.async_wait([weakSelf{weak_from_this()}]( | 
|  | const boost::system::error_code errorCode) { | 
|  | if (errorCode == boost::asio::error::operation_aborted) | 
|  | { | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (errorCode) | 
|  | { | 
|  | lg2::error("error code: '{ERROR_MESSAGE}'", "ERROR_MESSAGE", | 
|  | errorCode.message()); | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (auto self = weakSelf.lock()) | 
|  | { | 
|  | self->readAndProcessNVMeSensor(); | 
|  | } | 
|  | }); | 
|  | } | 
|  |  | 
|  | static double getTemperatureReading(int8_t reading) | 
|  | { | 
|  | if (reading == static_cast<int8_t>(0x80) || | 
|  | reading == static_cast<int8_t>(0x81)) | 
|  | { | 
|  | // 0x80 = No temperature data or temperature data is more the 5 s | 
|  | // old 0x81 = Temperature sensor failure | 
|  | return std::numeric_limits<double>::quiet_NaN(); | 
|  | } | 
|  |  | 
|  | return reading; | 
|  | } | 
|  |  | 
|  | void NVMeBasicContext::processResponse(std::shared_ptr<NVMeSensor>& sensor, | 
|  | void* msg, size_t len) | 
|  | { | 
|  | if (msg == nullptr || len < 6) | 
|  | { | 
|  | sensor->incrementError(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | uint8_t* messageData = static_cast<uint8_t*>(msg); | 
|  |  | 
|  | uint8_t status = messageData[0]; | 
|  | if (((status & NVME_MI_BASIC_SFLGS_DRIVE_NOT_READY) != 0) || | 
|  | ((status & NVME_MI_BASIC_SFLGS_DRIVE_FUNCTIONAL) == 0)) | 
|  | { | 
|  | sensor->markFunctional(false); | 
|  | return; | 
|  | } | 
|  |  | 
|  | double value = getTemperatureReading(messageData[2]); | 
|  | if (!std::isfinite(value)) | 
|  | { | 
|  | sensor->incrementError(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | sensor->updateValue(value); | 
|  | } |