| #ifndef THIRD_PARTY_GBMCWEB_INCLUDE_VM_WEBSOCKET_H_ |
| #define THIRD_PARTY_GBMCWEB_INCLUDE_VM_WEBSOCKET_H_ |
| |
| #include <cerrno> |
| #include <csignal> |
| #include <cstddef> |
| #include <memory> |
| #include <string> |
| #include <string_view> |
| #include <system_error> // NOLINT |
| |
| #include "boost/beast/core/flat_static_buffer.hpp" // NOLINT |
| #include "boost/process/async_pipe.hpp" // NOLINT |
| #include "boost/process/child.hpp" // NOLINT |
| #include "boost/process/io.hpp" // NOLINT |
| #include "app.hpp" |
| #include "logging.hpp" |
| #include "websocket.hpp" |
| |
| namespace crow { |
| namespace obmc_vm { |
| |
| // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) |
| static crow::websocket::Connection* session = nullptr; |
| |
| // The max network block device buffer size is 128kb plus 16bytes |
| // for the message header: |
| // https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md#simple-reply-message |
| static constexpr auto nbdBufferSize = (128 * 1024 + 16) * 4; |
| |
| class Handler : public std::enable_shared_from_this<Handler> { |
| public: |
| Handler(const std::string& mediaIn, boost::asio::io_context& ios) |
| : pipeOut(ios), |
| pipeIn(ios), |
| media(mediaIn), |
| outputBuffer(new boost::beast::flat_static_buffer<nbdBufferSize>), |
| inputBuffer(new boost::beast::flat_static_buffer<nbdBufferSize>) {} |
| |
| ~Handler() = default; |
| |
| Handler(const Handler&) = delete; |
| Handler(Handler&&) = delete; |
| Handler& operator=(const Handler&) = delete; |
| Handler& operator=(Handler&&) = delete; |
| |
| void doClose() { |
| // boost::process::child::terminate uses SIGKILL, need to send SIGTERM |
| // to allow the proxy to stop nbd-client and the USB device gadget. |
| int rc = kill(proxy.id(), SIGTERM); |
| if (rc != 0) { |
| BMCWEB_LOG_ERROR << "Failed to terminate nbd-proxy: " << errno; |
| return; |
| } |
| |
| proxy.wait(); |
| } |
| |
| void connect() { |
| std::error_code ec; |
| proxy = boost::process::child("/usr/sbin/nbd-proxy", media, |
| boost::process::std_out > pipeOut, |
| boost::process::std_in < pipeIn, ec); |
| if (ec) { |
| BMCWEB_LOG_ERROR << "Couldn't connect to nbd-proxy: " << ec.message(); |
| if (session != nullptr) { |
| session->close("Error connecting to nbd-proxy"); |
| } |
| return; |
| } |
| doWrite(); |
| doRead(); |
| } |
| |
| void doWrite() { |
| if (doingWrite) { |
| BMCWEB_LOG_DEBUG << "Already writing. Bailing out"; |
| return; |
| } |
| |
| if (inputBuffer->size() == 0) { |
| BMCWEB_LOG_DEBUG << "inputBuffer empty. Bailing out"; |
| return; |
| } |
| |
| doingWrite = true; |
| pipeIn.async_write_some( |
| inputBuffer->data(), |
| [this, self(shared_from_this())](boost::beast::error_code ec, |
| std::size_t bytesWritten) { |
| BMCWEB_LOG_DEBUG << "Wrote " << bytesWritten << "bytes"; |
| doingWrite = false; |
| inputBuffer->consume(bytesWritten); |
| |
| if (session == nullptr) { |
| return; |
| } |
| if (ec == boost::asio::error::eof) { |
| session->close("VM socket port closed"); |
| return; |
| } |
| if (ec) { |
| session->close("Error in writing to proxy port"); |
| BMCWEB_LOG_ERROR << "Error in VM socket write " << ec; |
| return; |
| } |
| doWrite(); |
| }); |
| } |
| |
| void doRead() { |
| std::size_t bytes = outputBuffer->capacity() - outputBuffer->size(); |
| |
| pipeOut.async_read_some( |
| outputBuffer->prepare(bytes), |
| [this, self(shared_from_this())](const boost::system::error_code& ec, |
| std::size_t bytesRead) { |
| BMCWEB_LOG_DEBUG << "Read done. Read " << bytesRead << " bytes"; |
| if (ec) { |
| BMCWEB_LOG_ERROR << "Couldn't read from VM port: " << ec; |
| if (session != nullptr) { |
| session->close("Error in connecting to VM port"); |
| } |
| return; |
| } |
| if (session == nullptr) { |
| return; |
| } |
| |
| outputBuffer->commit(bytesRead); |
| std::string_view payload( |
| static_cast<const char*>(outputBuffer->data().data()), bytesRead); |
| session->sendBinary(payload); |
| outputBuffer->consume(bytesRead); |
| |
| doRead(); |
| }); |
| } |
| |
| boost::process::async_pipe pipeOut; |
| boost::process::async_pipe pipeIn; |
| boost::process::child proxy; |
| std::string media; |
| bool doingWrite{false}; |
| |
| std::unique_ptr<boost::beast::flat_static_buffer<nbdBufferSize>> outputBuffer; |
| std::unique_ptr<boost::beast::flat_static_buffer<nbdBufferSize>> inputBuffer; |
| }; |
| |
| // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) |
| static std::shared_ptr<Handler> handler; |
| |
| inline void requestRoutes(App& app) { |
| BMCWEB_ROUTE(app, "/vm/0/0") |
| .privileges({{"ConfigureComponents", "ConfigureManager"}}) |
| .websocket() |
| .onopen([](crow::websocket::Connection& conn) { |
| BMCWEB_LOG_DEBUG << "Connection " << &conn << " opened"; |
| |
| if (session != nullptr) { |
| conn.close("Session already connected"); |
| return; |
| } |
| |
| if (handler != nullptr) { |
| conn.close("Handler already running"); |
| return; |
| } |
| |
| session = &conn; |
| |
| // media is the last digit of the endpoint /vm/0/0. A future |
| // enhancement can include supporting different endpoint values. |
| const char* media = "0"; |
| handler = std::make_shared<Handler>(media, conn.getIoContext()); |
| handler->connect(); |
| }) |
| .onclose( |
| [](crow::websocket::Connection& conn, const std::string& /*reason*/) { |
| if (&conn != session) { |
| return; |
| } |
| |
| session = nullptr; |
| handler->doClose(); |
| handler->inputBuffer->clear(); |
| handler->outputBuffer->clear(); |
| handler.reset(); |
| }) |
| .onmessage([](crow::websocket::Connection& conn, const std::string& data, |
| bool) { |
| if (data.length() > |
| handler->inputBuffer->capacity() - handler->inputBuffer->size()) { |
| BMCWEB_LOG_ERROR << "Buffer overrun when writing " << data.length() |
| << " bytes"; |
| conn.close("Buffer overrun"); |
| return; |
| } |
| |
| boost::asio::buffer_copy(handler->inputBuffer->prepare(data.size()), |
| boost::asio::buffer(data)); |
| handler->inputBuffer->commit(data.size()); |
| handler->doWrite(); |
| }); |
| } |
| |
| } // namespace obmc_vm |
| } // namespace crow |
| |
| #endif // THIRD_PARTY_GBMCWEB_INCLUDE_VM_WEBSOCKET_H_ |