diff --git a/src/fsfw/osal/linux/CMakeLists.txt b/src/fsfw/osal/linux/CMakeLists.txt index 0fb66b3ed..fa2e8aa05 100644 --- a/src/fsfw/osal/linux/CMakeLists.txt +++ b/src/fsfw/osal/linux/CMakeLists.txt @@ -16,6 +16,7 @@ target_sources(${LIB_FSFW_NAME} Timer.cpp tcpipHelpers.cpp unixUtility.cpp + CommandExecutor.cpp ) find_package(Threads REQUIRED) diff --git a/src/fsfw/osal/linux/CommandExecutor.cpp b/src/fsfw/osal/linux/CommandExecutor.cpp new file mode 100644 index 000000000..bd23f97d7 --- /dev/null +++ b/src/fsfw/osal/linux/CommandExecutor.cpp @@ -0,0 +1,180 @@ +#include "CommandExecutor.h" + +#include "fsfw/serviceinterface.h" +#include "fsfw/container/SimpleRingBuffer.h" +#include "fsfw/container/DynamicFIFO.h" + +#include + +#include + +CommandExecutor::CommandExecutor(const size_t maxSize): +readVec(maxSize) { + waiter.events = POLLIN; +} + +ReturnValue_t CommandExecutor::load(std::string command, bool blocking, bool printOutput) { + if(state == States::PENDING) { + return COMMAND_PENDING; + } + + currentCmd = command; + this->blocking = blocking; + this->printOutput = printOutput; + if(state == States::IDLE) { + state = States::COMMAND_LOADED; + } + return HasReturnvaluesIF::RETURN_OK; +} + +ReturnValue_t CommandExecutor::execute() { + if(state == States::IDLE) { + return NO_COMMAND_LOADED_OR_PENDING; + } + else if(state == States::PENDING) { + return COMMAND_PENDING; + } + currentCmdFile = popen(currentCmd.c_str(), "r"); + if(currentCmdFile == nullptr) { + lastError = errno; + return HasReturnvaluesIF::RETURN_FAILED; + } + if(blocking) { + return executeBlocking(); + } + else { + currentFd = fileno(currentCmdFile); + waiter.fd = currentFd; + } + return HasReturnvaluesIF::RETURN_OK; +} + +ReturnValue_t CommandExecutor::close() { + if(state == States::PENDING) { + // Attempt to close process, irrespective of if it is running or not + if(currentCmdFile != nullptr) { + pclose(currentCmdFile); + } + } + return HasReturnvaluesIF::RETURN_OK; +} + +void CommandExecutor::printLastError(std::string funcName) const { + if(lastError != 0) { + sif::error << funcName << " pclose failed with code " << + lastError << ": " << strerror(lastError) << std::endl; + } +} + +void CommandExecutor::setRingBuffer(SimpleRingBuffer *ringBuffer, + DynamicFIFO* sizesFifo) { + this->ringBuffer = ringBuffer; + this->sizesFifo = sizesFifo; +} + +ReturnValue_t CommandExecutor::check(bool& bytesRead) { + if(blocking) { + return HasReturnvaluesIF::RETURN_OK; + } + switch(state) { + case(States::IDLE): + case(States::COMMAND_LOADED): { + return NO_COMMAND_LOADED_OR_PENDING; + } + case(States::PENDING): { + break; + } + } + + int result = poll(&waiter, 1, 0); + switch(result) { + case(0): { + return HasReturnvaluesIF::RETURN_OK; + break; + } + case(1): { + if (waiter.revents & POLLIN) { + ssize_t readBytes = read(currentFd, readVec.data(), readVec.size()); + if(readBytes == 0) { + // Should not happen + sif::warning << "CommandExecutor::check: " + "No bytes read after poll event.." << std::endl; + break; + } + else if(readBytes > 0) { + bytesRead = true; + if(printOutput) { + // It is assumed the command output is line terminated + sif::info << currentCmd << " | " << readVec.data(); + } + if(ringBuffer != nullptr) { + ringBuffer->writeData(reinterpret_cast( + readVec.data()), readBytes); + } + if(sizesFifo != nullptr) { + if(not sizesFifo->full()) { + sizesFifo->insert(readBytes); + } + } + return BYTES_READ; + } + else { + // Should also not happen + sif::warning << "CommandExecutor::check: Error " << errno << ": " << + strerror(errno) << std::endl; + } + } + else if(waiter.revents & POLLERR) { + sif::warning << "CommandExecuter::check: Poll error" << std::endl; + return COMMAND_ERROR; + } + else if(waiter.revents & POLLHUP) { + int result = pclose(currentCmdFile); + if(result != 0) { + lastError = result; + return HasReturnvaluesIF::RETURN_FAILED; + } + state = States::IDLE; + currentCmdFile = nullptr; + currentFd = 0; + return EXECUTION_FINISHED; + } + break; + } + } + return HasReturnvaluesIF::RETURN_OK; +} + +void CommandExecutor::reset() { + CommandExecutor::close(); + currentCmdFile = nullptr; + currentFd = 0; + state = States::IDLE; +} + +int CommandExecutor::getLastError() const { + return this->lastError; +} + +ReturnValue_t CommandExecutor::executeBlocking() { + while(fgets(readVec.data(), readVec.size(), currentCmdFile) != nullptr) { + std::string output(readVec.data()); + if(printOutput) { + sif::info << currentCmd << " | " << output; + } + if(ringBuffer != nullptr) { + ringBuffer->writeData(reinterpret_cast(output.data()), output.size()); + } + if(sizesFifo != nullptr) { + if(not sizesFifo->full()) { + sizesFifo->insert(output.size()); + } + } + } + int result = pclose(currentCmdFile); + if(result != 0) { + lastError = result; + return HasReturnvaluesIF::RETURN_FAILED; + } + return EXECUTION_FINISHED; +} diff --git a/src/fsfw/osal/linux/CommandExecutor.h b/src/fsfw/osal/linux/CommandExecutor.h new file mode 100644 index 000000000..2810b67dc --- /dev/null +++ b/src/fsfw/osal/linux/CommandExecutor.h @@ -0,0 +1,133 @@ +#ifndef FSFW_SRC_FSFW_OSAL_LINUX_COMMANDEXECUTOR_H_ +#define FSFW_SRC_FSFW_OSAL_LINUX_COMMANDEXECUTOR_H_ + +#include "fsfw/returnvalues/HasReturnvaluesIF.h" +#include "fsfw/returnvalues/FwClassIds.h" + +#include + +#include +#include + +class SimpleRingBuffer; +template class DynamicFIFO; + +/** + * @brief Helper class to execute shell commands in blocking and non-blocking mode + * @details + * This class is able to execute processes by using the Linux popen call. It also has the + * capability of writing the read output of a process into a provided ring buffer. + * + * The executor works by first loading the command which should be executed and specifying + * whether it should be executed blocking or non-blocking. After that, execution can be started + * with the execute command. In blocking mode, the execute command will block until the command + * has finished + */ +class CommandExecutor { +public: + enum class States { + IDLE, + COMMAND_LOADED, + PENDING + }; + + static constexpr uint8_t CLASS_ID = CLASS_ID::LINUX_OSAL; + + //! [EXPORT] : [COMMENT] Execution of the current command has finished + static constexpr ReturnValue_t EXECUTION_FINISHED = + HasReturnvaluesIF::makeReturnCode(CLASS_ID, 0); + + //! [EXPORT] : [COMMENT] Command is pending. This will also be returned if the user tries + //! to load another command but a command is still pending + static constexpr ReturnValue_t COMMAND_PENDING = + HasReturnvaluesIF::makeReturnCode(CLASS_ID, 1); + //! [EXPORT] : [COMMENT] Some bytes have been read from the executing process + static constexpr ReturnValue_t BYTES_READ = + HasReturnvaluesIF::makeReturnCode(CLASS_ID, 2); + //! [EXPORT] : [COMMENT] Command execution failed + static constexpr ReturnValue_t COMMAND_ERROR = + HasReturnvaluesIF::makeReturnCode(CLASS_ID, 3); + //! [EXPORT] : [COMMENT] + static constexpr ReturnValue_t NO_COMMAND_LOADED_OR_PENDING = + HasReturnvaluesIF::makeReturnCode(CLASS_ID, 4); + static constexpr ReturnValue_t PCLOSE_CALL_ERROR = + HasReturnvaluesIF::makeReturnCode(CLASS_ID, 6); + + /** + * Constructor. Is initialized with maximum size of internal buffer to read data from the + * executed process. + * @param maxSize + */ + CommandExecutor(const size_t maxSize); + + /** + * Load a new command which should be executed + * @param command + * @param blocking + * @param printOutput + * @return + */ + ReturnValue_t load(std::string command, bool blocking, bool printOutput = true); + /** + * Execute the loaded command. + * @return + * - In blocking mode, it will return RETURN_FAILED if + * the result of the system call was not 0. The error value can be accessed using + * getLastError + * - In non-blocking mode, this call will start + * the execution and then return RETURN_OK + */ + ReturnValue_t execute(); + /** + * Only used in non-blocking mode. Checks the currently running command. + * @param bytesRead Will be set to the number of bytes read, if bytes have been read + * @return + * - BYTES_READ if bytes have been read from the executing process. It is recommended to call + * check again after this + * - RETURN_OK execution is pending, but no bytes have been read from the executing process + * - RETURN_FAILED if execution has failed, error value can be accessed using getLastError + * - EXECUTION_FINISHED if the process was executed successfully + * - COMMAND_ERROR internal poll error + */ + ReturnValue_t check(bool& bytesRead); + /** + * Abort the current command. Should normally not be necessary, check can be used to find + * out whether command execution was successful + * @return RETURN_OK + */ + ReturnValue_t close(); + + int getLastError() const; + void printLastError(std::string funcName) const; + + /** + * Assign a ring buffer and a FIFO which will be filled by the executor with the output + * read from the started process + * @param ringBuffer + * @param sizesFifo + */ + void setRingBuffer(SimpleRingBuffer* ringBuffer, DynamicFIFO* sizesFifo); + + /** + * Reset the executor. This calls close internally and then reset the state machine so new + * commands can be loaded and executed + */ + void reset(); +private: + std::string currentCmd; + bool blocking = true; + FILE* currentCmdFile = nullptr; + int currentFd = 0; + bool printOutput = true; + std::vector readVec; + struct pollfd waiter {}; + SimpleRingBuffer* ringBuffer = nullptr; + DynamicFIFO* sizesFifo = nullptr; + + States state = States::IDLE; + int lastError = 0; + + ReturnValue_t executeBlocking(); +}; + +#endif /* FSFW_SRC_FSFW_OSAL_LINUX_COMMANDEXECUTOR_H_ */ diff --git a/src/fsfw/returnvalues/FwClassIds.h b/src/fsfw/returnvalues/FwClassIds.h index af32f9a76..7f355c40d 100644 --- a/src/fsfw/returnvalues/FwClassIds.h +++ b/src/fsfw/returnvalues/FwClassIds.h @@ -72,6 +72,7 @@ enum: uint8_t { PUS_SERVICE_3, //PUS3 PUS_SERVICE_9, //PUS9 FILE_SYSTEM, //FILS + LINUX_OSAL, //UXOS HAL_SPI, //HSPI HAL_UART, //HURT HAL_I2C, //HI2C