Program Listing for File Network.cpp

Return to documentation for file (gui/Network/Network.cpp)

#include "Network.hpp"
#include "logging/Logger.hpp"

#include <netinet/in.h>

#include <sstream>
#include <stdexcept>

constexpr int FD_PIPE_EXIT = 0;
constexpr int FD_SERVER_IN = 1;
constexpr int FD_PIPE_NETWORK = 2;
constexpr int FD_EXIT_IN = 0;
constexpr int FD_EXIT_OUT = 1;
constexpr int FD_SERVER_OUT = 0;

Network::Network(int port, std::string hostname, std::shared_ptr<API> &data)
  : _port(port), _hostname(std::move(hostname)), _api(data)
{
  // Create server socket
  _fdServer = socket(AF_INET, SOCK_STREAM, 0);
  if (_fdServer == -1)
    throw std::
      runtime_error("Error: socket, Function: Network, File: Network.cpp");

  // Set socket options
  memset(&serverAddr, 0, sizeof(serverAddr));
  serverAddr.sin_family = AF_INET;
  serverAddr.sin_port = htons(_port);
  socklen_t sockLen = sizeof(serverAddr);
  // Convert hostname to IP address
  if (inet_pton(AF_INET, _hostname.c_str(), &serverAddr.sin_addr) != 1)
    throw std::runtime_error(
      "Error: inet_pton failed. Function: RunNetwork, File: Network.cpp");

  // Try to connect to the server in a loop until successful or unrecoverable
  // error
  while (true) {
    if (connect(_fdServer, (const sockaddr *)(&serverAddr), sockLen) == 0)
      break;  // Connected successfully
    // Check for recoverable errors
    if (errno == EINTR || errno == ECONNREFUSED || errno == ETIMEDOUT) {
      std::this_thread::sleep_for(std::chrono::milliseconds(500));
      continue;
    }
    // Unrecoverable error
    throw std::runtime_error(
      std::string(
        "Error: connect failed. Function: Network, File: "
        "Network.cpp: (")
      + strerror(errno) + ").");
  }

  // Create pipe for thread exit
  if (pipe(_pipeFdExit.data()) == -1)
    throw std::runtime_error(
      "Error: pipe failed. Function: Network, File: Network.cpp");

  // Initialize poll structures
  _pollInFd[FD_PIPE_EXIT].fd = _pipeFdExit[FD_EXIT_IN];
  _pollInFd[FD_SERVER_IN].fd = _fdServer;
  _pollInFd[FD_PIPE_NETWORK].fd = _api->GetInFd();
  for (pollfd &pollFd: _pollInFd) {
    pollFd.events = POLLIN | POLLHUP;  // monitor for input and hangup
    pollFd.revents = 0;
  }

  // Initialize pollOutFd for sending messages
  _pollOutFd[FD_SERVER_OUT].events = POLLOUT;
  _pollOutFd[FD_SERVER_OUT].fd = _fdServer;
  _pollOutFd[FD_SERVER_OUT].revents = 0;
}

Network::~Network()
{
  _networkThread.join();  // wait for the network thread to finish

  shutdown(_fdServer, SHUT_RDWR);  // shut both end read and write
  read(_fdServer, nullptr, 0);     // clear buffer
  close(_fdServer);                // close socket

  close(_pipeFdExit[FD_EXIT_IN]);   // close the read end of the pipe
  close(_pipeFdExit[FD_EXIT_OUT]);  // close the write end of the pipe
}

void Network::ServerHandshake()
{
  if (poll(_pollInFd.data(), _pollInFd.size(), -1) == -1)
    throw std::runtime_error(
      "Error: poll failed. Function: RunNetwork, File: Network.cpp");

  if (_pollInFd[FD_SERVER_IN].revents & POLLIN)
    Log::inf << "Message received: " << Log::cleanString(ReceiveMessage());

  SendMessage("GRAPHIC\n");
}

void Network::Run()
{
  if (!_networkThread.joinable())
    _networkThread = std::thread(&Network::RunNetworkInternal, this);
}

void Network::RunNetworkInternal()
{
  ServerHandshake();

  bool isRunning = true;
  while (isRunning) {
    if (poll(_pollInFd.data(), _pollInFd.size(), -1) == -1)
      throw std::runtime_error(
        "Error: poll failed. Function: RunNetwork, File: Network.cpp");

    // server disconnected
    if (_pollInFd[FD_SERVER_IN].revents & POLLHUP) {
      Log::failed << "Server disconnected.";
      isRunning = false;
      continue;
    }

    // exit event
    if (_pollInFd[FD_PIPE_EXIT].revents & POLLIN) {
      std::array<char, 10> buffer;
      if (read(_pollInFd[FD_PIPE_EXIT].fd, buffer.data(), buffer.size()) == -1)
        throw std::runtime_error(
          "Error: read failed. Function: RunNetwork, File: Network.cpp");
      isRunning = false;
      Log::inf << "End of network thread requested.";
    }

    // server message
    if (_pollInFd[FD_SERVER_IN].revents & POLLIN) {
      std::string message = ReceiveMessage();
      try {
        _api->ParseManageCommand(message);
      } catch (const std::exception &e) {
        Log::failed << "Exception in ParseManageCommande: " << e.what();
      }
    }

    // network pipe message
    if (_pollInFd[FD_PIPE_NETWORK].revents & POLLIN) {
      static std::string leftover;
      std::string message;
      std::array<char, 1024> buffer;

      ssize_t bytesRead = read(
        _api->GetInFd(), buffer.data(), buffer.size() - 1);
      if (bytesRead < 0)
        throw std::runtime_error(
          "Error: read failed. Function: RunNetwork, File: Network.cpp");
      // all messages are separated by '\n'
      buffer[bytesRead] = '\0';
      std::stringstream ss(leftover + std::string(buffer.data(), bytesRead));
      while (std::getline(ss, message, '\n'))
        if (!message.empty()) {
          // Log::inf
          //   << "Message received from pipe: " << Log::cleanString(message);
          SendMessage(message + "\n");
        }
      // if there is still data in the stream, save it for the next iteration
      // (e.g., if the last message did not end with '\n')
      leftover.clear();
      if (!ss.eof())
        leftover = ss.str().substr(ss.tellg());
    }
  }
}

void Network::SendMessage(const std::string &msg)
{
  if (poll(_pollOutFd.data(), _pollOutFd.size(), -1) == -1)
    throw std::runtime_error(
      "Error: poll failed, Function: SendMessage, File: Network.cpp");

  // Check if the socket is ready for writing
  if (!(_pollOutFd[FD_SERVER_OUT].revents & POLLOUT))
    throw std::runtime_error(
      "Error: socket not ready for writing, Function: SendMessage, File: "
      "Network.cpp");

  if (send(_fdServer, msg.c_str(), msg.size(), 0) == -1)
    throw std::
      runtime_error("Error: send, Function: SendMessage, File: Network.cpp");
  // Log::inf << "Message sent : " << Log::cleanString(msg);
}

std::string Network::ReceiveMessage() const
{
  constexpr std::size_t chunk_size = 1024;
  std::vector<char> buffer;

  while (true) {
    std::vector<char> chunk(chunk_size);
    ssize_t bytes_received = recv(_fdServer, chunk.data(), chunk.size(), 0);
    if (bytes_received < 0) {
      if (errno == EINTR)
        continue;
      throw std::runtime_error(
        "Error: recv failed, Function: ReceiveMessage, File: Network.cpp");
    }
    if (bytes_received == 0)
      break;
    buffer.insert(buffer.end(), chunk.begin(), chunk.begin() + bytes_received);
    if ((size_t)(bytes_received) < chunk_size)
      break;
  }

  return std::string(buffer.begin(), buffer.end());
}

void Network::RequestStop()
{
  Log::inf << "Requesting stop of network thread.\n";
  write(_pipeFdExit[FD_EXIT_OUT], "x", 1);
}