blob: c2ce8a8d2d7563ed0db95e2b0b2410357a2e2d3f [file]
#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_