From: Daniel Karbach Date: Wed, 16 Nov 2016 15:22:58 +0000 (+0100) Subject: add TCP based CLI X-Git-Url: https://git.localhorst.tv/?a=commitdiff_plain;h=b07f3c123fff221edeffb4864bab7db88d0d1f4d;p=blank.git add TCP based CLI which currently can't do anything, because the sole command needs a player ^^ --- diff --git a/doc/running b/doc/running index 60dfe03..426c463 100644 --- a/doc/running +++ b/doc/running @@ -87,6 +87,10 @@ Network --port port number to connection to (client) or listen on (server) +--cmd-port + port number to listen on for command connections + the default of 0 disables this feature + --player-name use given name to identify with the server (client mode) default player name is "default" diff --git a/src/app/Config.hpp b/src/app/Config.hpp index 5261a44..d584766 100644 --- a/src/app/Config.hpp +++ b/src/app/Config.hpp @@ -30,6 +30,7 @@ struct Config { std::string host = "localhost"; std::uint16_t port = 12354; + std::uint16_t cmd_port = 0; } net; diff --git a/src/app/runtime.cpp b/src/app/runtime.cpp index 2481f93..75eeec5 100644 --- a/src/app/runtime.cpp +++ b/src/app/runtime.cpp @@ -75,6 +75,10 @@ void Config::Load(std::istream &is) { int port; in.ReadNumber(port); net.port = port; + } else if (name == "net.cmd_port") { + int port; + in.ReadNumber(port); + net.cmd_port = port; } else if (name == "player.name") { in.ReadString(player.name); } else if (name == "video.dblbuf") { @@ -104,6 +108,7 @@ void Config::Save(std::ostream &out) { out << "input.yaw_sensitivity = " << input.yaw_sensitivity << ';' << std::endl; out << "net.host = \"" << net.host << "\";" << std::endl; out << "net.port = " << net.port << ';' << std::endl; + out << "net.cmd_port = " << net.cmd_port << ';' << std::endl; out << "player.name = \"" << player.name << "\";" << std::endl; out << "video.dblbuf = " << (video.dblbuf ? "on" : "off") << ';' << std::endl; out << "video.vsync = " << (video.vsync ? "on" : "off") << ';' << std::endl; @@ -240,6 +245,14 @@ void Runtime::ReadArgs(int argc, const char *const *argv) { } else { config.game.net.port = strtoul(argv[i], nullptr, 10); } + } else if (strcmp(param, "cmd-port") == 0) { + ++i; + if (i >= argc || argv[i] == nullptr || argv[i][0] == '\0') { + cerr << "missing argument to --cmd-port" << endl; + error = true; + } else { + config.game.net.cmd_port = strtoul(argv[i], nullptr, 10); + } } else if (strcmp(param, "player-name") == 0) { ++i; if (i >= argc || argv[i] == nullptr || argv[i][0] == '\0') { diff --git a/src/net/tcp.cpp b/src/net/tcp.cpp new file mode 100644 index 0000000..f9e039b --- /dev/null +++ b/src/net/tcp.cpp @@ -0,0 +1,201 @@ +#include "tcp.hpp" + +#include "../app/init.hpp" + +#include + +using namespace std; + + +namespace blank { +namespace tcp { + +Socket::Socket() +: sock(nullptr) { + +} + +Socket::Socket(unsigned short port) +: sock(nullptr) { + IPaddress ip; + if (SDLNet_ResolveHost(&ip, nullptr, port) == -1) { + throw NetError("failed to resolve local host"); + } + sock = SDLNet_TCP_Open(&ip); + if (!sock) { + throw NetError("failed to open local socket"); + } +} + +Socket::Socket(TCPsocket sock) +: sock(sock) { + +} + +Socket::~Socket() noexcept { + if (sock) { + SDLNet_TCP_Close(sock); + } +} + +Socket::Socket(Socket &&other) noexcept +: sock(other.sock) { + other.sock = nullptr; +} + +Socket &Socket::operator =(Socket &&other) noexcept { + swap(sock, other.sock); + return *this; +} + + +Socket Socket::Accept() noexcept { + return Socket(SDLNet_TCP_Accept(sock)); +} + +bool Socket::Ready() const noexcept { + return SDLNet_SocketReady(sock); +} + +size_t Socket::Recv(void *buf, std::size_t max_len) { + const int len = SDLNet_TCP_Recv(sock, buf, max_len); + if (len < 0) { + throw NetError("TCP socket recv"); + } + return len; +} + +size_t Socket::Send(const void *buf, size_t max_len) { + /// TODO: make TCP send non-blocking + const int len = SDLNet_TCP_Send(sock, buf, max_len); + if (len < int(max_len)) { + throw NetError("TCP socket send"); + } + return len; +} + + +int Socket::AddTo(SDLNet_SocketSet set) { + return SDLNet_TCP_AddSocket(set, sock); +} + +int Socket::RemoveFrom(SDLNet_SocketSet set) { + return SDLNet_TCP_DelSocket(set, sock); +} + + +Pool::Pool(int max_conn, size_t buf_siz) +: set(SDLNet_AllocSocketSet(max_conn)) +, buffer(buf_siz, '\0') +, connections() +, use_conn(0) +, max_conn(max_conn) +, buf_siz(buf_siz) { + if (!set) { + throw runtime_error("failed to allocate socket set"); + } +} + +Pool::~Pool() noexcept { + SDLNet_FreeSocketSet(set); +} + + +void Pool::AddConnection(Socket sock, IOHandler *handler) { + if (FreeSlots() == 0) { + Resize(TotalSlots() * 2); + } + int num = sock.AddTo(set); + if (num < 0) { + throw NetError("failed to add socket to set"); + } + use_conn = num; + connections.emplace_back(move(sock), handler); + handler->OnCreate(connections.back().first); +} + +void Pool::Send() { + for (auto i = connections.begin(); i != connections.end(); ++i) { + if (i->second->Closed()) { + continue; + } + + try { + i->second->OnSend(i->first); + } catch (...) { + i->second->OnError(i->first); + } + } +} + +bool Pool::Check(unsigned long timeout) { + // SDL_net considers checking an empty set an error, so + // we're checking that ourselves + if (OccupiedSlots() == 0) { + return false; + } + + int num = SDLNet_CheckSockets(set, timeout); + if (num < 0) { + throw NetError("error checking sockets"); + } + return num > 0; +} + +void Pool::Receive() { + for (auto i = connections.begin(); i != connections.end(); ++i) { + if (!i->first.Ready() || i->second->Closed()) { + continue; + } + + try { + i->second->OnRecv(i->first); + } catch (...) { + i->second->OnError(i->first); + } + } +} + +void Pool::Clean() { + for (auto i = connections.begin(); i != connections.end();) { + if (i->second->Closed()) { + int num = i->first.RemoveFrom(set); + if (num < 0) { + throw NetError("failed to remove socket from set"); + } + use_conn = num; + i->second->OnRemove(i->first); + i = connections.erase(i); + } else { + ++i; + } + } +} + + +void Pool::Resize(int new_max) { + if (new_max < max_conn) { + return; + } + + int new_size = max(new_max, max_conn * 2); + SDLNet_SocketSet new_set(SDLNet_AllocSocketSet(new_size)); + if (!new_set) { + throw NetError("failed to allocate socket set"); + } + + for (auto &conn : connections) { + if (conn.first.AddTo(new_set) == -1) { + NetError error("failed to migrate socket to new set"); + SDLNet_FreeSocketSet(new_set); + throw error; + } + } + + SDLNet_FreeSocketSet(set); + set = new_set; + max_conn = new_size; +} + +} +} diff --git a/src/net/tcp.hpp b/src/net/tcp.hpp new file mode 100644 index 0000000..5b5f878 --- /dev/null +++ b/src/net/tcp.hpp @@ -0,0 +1,126 @@ +#ifndef BLANK_NET_TCP_HPP_ +#define BLANK_NET_TCP_HPP_ + +#include +#include +#include +#include + + +namespace blank { +namespace tcp { + +/// all failing functions throw NetError +class Socket { + +public: + /// create an empty socket that is not connected to anything + Socket(); + /// create TCP socket bound to given port + explicit Socket(unsigned short port); +private: + /// wrap given SDLNet TCP socket + /// for use with Accept() + explicit Socket(TCPsocket sock); +public: + ~Socket() noexcept; + + Socket(const Socket &) = delete; + Socket &operator =(const Socket &) = delete; + + Socket(Socket &&) noexcept; + Socket &operator =(Socket &&) noexcept; + + explicit operator bool() const noexcept { return sock; } + + bool operator ==(const Socket &other) const noexcept { + return sock == other.sock; + } + bool operator <(const Socket &other) const noexcept { + return sock < other.sock; + } + +public: + /// create a socket for an incoming connection + /// @return an empty socket if there are none + Socket Accept() noexcept; + /// check if there is data available to read + bool Ready() const noexcept; + /// receive data into given buffer + /// @return number of bytes read, at most max_len + /// non-blocking if Ready() is true + std::size_t Recv(void *buf, std::size_t max_len); + /// send data from given buffer, at most max_len bytes + /// @return number of bytes written + /// may be less than len as soon as I get to + /// making it non-blocking + std::size_t Send(const void *buf, std::size_t max_len); + + int AddTo(SDLNet_SocketSet); + int RemoveFrom(SDLNet_SocketSet); + +private: + TCPsocket sock; + +}; + + +struct IOHandler { + + virtual ~IOHandler() = default; + + void Close() noexcept { closed = true; } + bool Closed() const noexcept { return closed; } + + virtual void OnCreate(Socket &) { } + virtual void OnRemove(Socket &) noexcept { } + + virtual void OnSend(Socket &) { }; + virtual void OnRecv(Socket &) { }; + virtual void OnError(Socket &) noexcept { Close(); } + +private: + bool closed = false; + +}; + + +class Pool { + +public: + using ConnectionSet = std::list>; + +public: + explicit Pool(int max_conn = 32, std::size_t buf_siz = 1500); + ~Pool() noexcept; + + Pool(const Pool &) = delete; + Pool &operator =(const Pool &) = delete; + +public: + void AddConnection(Socket, IOHandler *); + void Send(); + bool Check(unsigned long timeout); + void Receive(); + void Clean(); + + int FreeSlots() const noexcept { return max_conn - use_conn; } + int OccupiedSlots() const noexcept { return use_conn; } + int TotalSlots() const noexcept { return max_conn; } + /// reallocate the pool to accomodate at least new_max sockets + void Resize(int new_max); + +private: + SDLNet_SocketSet set; + std::string buffer; + ConnectionSet connections; + int use_conn; + int max_conn; + std::size_t buf_siz; + +}; + +} +} + +#endif diff --git a/src/server/Server.hpp b/src/server/Server.hpp index 9b89ab1..f81c9ce 100644 --- a/src/server/Server.hpp +++ b/src/server/Server.hpp @@ -7,6 +7,7 @@ #include "../world/WorldManipulator.hpp" #include +#include #include #include @@ -15,6 +16,7 @@ namespace blank { class ChunkIndex; class CLIContext; +class CommandService; class Model; class Player; class WorldSave; @@ -77,6 +79,7 @@ private: const Model *player_model; CLI cli; + std::unique_ptr cmd_srv; }; diff --git a/src/server/net.cpp b/src/server/net.cpp index e88ca1f..f5e974d 100644 --- a/src/server/net.cpp +++ b/src/server/net.cpp @@ -6,6 +6,7 @@ #include "../geometry/distance.hpp" #include "../io/WorldSave.hpp" #include "../model/Model.hpp" +#include "../shared/CommandService.hpp" #include "../world/ChunkIndex.hpp" #include "../world/Entity.hpp" #include "../world/World.hpp" @@ -647,7 +648,7 @@ uint16_t ClientConnection::SendMessage(uint8_t type, uint32_t from, const string NetworkCLIFeedback::NetworkCLIFeedback(Player &p, ClientConnection &c) -: CLIContext(p) +: CLIContext(&p) , conn(c) { } @@ -682,7 +683,8 @@ Server::Server( , spawn_index(world.Chunks().MakeIndex(wc.spawn, 3)) , save(save) , player_model(nullptr) -, cli(world) { +, cli(world) +, cmd_srv() { #pragma GCC diagnostic pop if (!serv_set) { throw NetError("SDLNet_AllocSocketSet"); @@ -702,6 +704,10 @@ Server::Server( serv_pack.data = new Uint8[sizeof(Packet)]; serv_pack.maxlen = sizeof(Packet); + + if (conf.cmd_port) { + cmd_srv.reset(new CommandService(cli, conf.cmd_port)); + } } Server::~Server() { @@ -719,10 +725,16 @@ Server::~Server() { void Server::Wait(int dt) noexcept { SDLNet_CheckSockets(serv_set, dt); + if (cmd_srv) { + cmd_srv->Wait(0); + } } bool Server::Ready() noexcept { - return SDLNet_CheckSockets(serv_set, 0) > 0; + if (SDLNet_CheckSockets(serv_set, 0) > 0) { + return true; + } + return cmd_srv && cmd_srv->Ready(); } void Server::Handle() { @@ -735,6 +747,9 @@ void Server::Handle() { // a boo boo happened throw NetError("SDLNet_UDP_Recv"); } + if (cmd_srv) { + cmd_srv->Handle(); + } } void Server::HandlePacket(const UDPpacket &udp_pack) { @@ -774,6 +789,9 @@ void Server::Update(int dt) { ++client; } } + if (cmd_srv) { + cmd_srv->Send(); + } } void Server::SetPlayerModel(const Model &m) noexcept { diff --git a/src/shared/CLIContext.hpp b/src/shared/CLIContext.hpp index 8de432b..5308bb2 100644 --- a/src/shared/CLIContext.hpp +++ b/src/shared/CLIContext.hpp @@ -11,11 +11,14 @@ class Player; class CLIContext { public: - explicit CLIContext(Player &p) + explicit CLIContext(Player *p = nullptr) : player(p) { } + /// check if this context associates a player + bool HasPlayer() { return player; } /// get the player responsible for all this - Player &GetPlayer() { return player; } + /// only valid if HasPlayer() returns true + Player &GetPlayer() { return *player; } /// an error has happened and the player should be notified virtual void Error(const std::string &) = 0; @@ -29,7 +32,7 @@ public: virtual void Broadcast(const std::string &) = 0; private: - Player &player; + Player *player; }; diff --git a/src/shared/CommandBuffer.hpp b/src/shared/CommandBuffer.hpp new file mode 100644 index 0000000..316fe6b --- /dev/null +++ b/src/shared/CommandBuffer.hpp @@ -0,0 +1,46 @@ +#ifndef BLANK_SHARED_COMMANDBUFFER_HPP_ +#define BLANK_SHARED_COMMANDBUFFER_HPP_ + +#include "CLIContext.hpp" +#include "../net/tcp.hpp" + +#include + + +namespace blank { + +/// Turns a tcp stream into commands and writes their +/// output back to the stream. +/// Instances delete themselves when OnRemove(tcp::Socket &) +/// is called, so make sure it was either allocated with new +/// and isn't dereferenced after removal or OnRemove is never +/// called. +class CommandBuffer +: public CLIContext +, public tcp::IOHandler { + +public: + explicit CommandBuffer(CLI &); + ~CommandBuffer() override; + + // CLIContext implementation + void Error(const std::string &) override; + void Message(const std::string &) override; + void Broadcast(const std::string &) override; + + /// IOHandler implementation + void OnSend(tcp::Socket &) override; + void OnRecv(tcp::Socket &) override; + void OnRemove(tcp::Socket &) noexcept override; + +private: + CLI &cli; + std::string write_buffer; + std::string read_buffer; + std::size_t head; + +}; + +} + +#endif diff --git a/src/shared/CommandService.hpp b/src/shared/CommandService.hpp new file mode 100644 index 0000000..38531ed --- /dev/null +++ b/src/shared/CommandService.hpp @@ -0,0 +1,38 @@ +#ifndef BLANK_SHARED_COMMANDSERVICE_HPP_ +#define BLANK_SHARED_COMMANDSERVICE_HPP_ + +#include "../net/tcp.hpp" + + +namespace blank { + +class CLI; + +class CommandService +: public tcp::IOHandler { + +public: + CommandService(CLI &, unsigned short port); + ~CommandService(); + +public: + /// wait on incoming data for at most timeout ms + void Wait(int timeout) noexcept; + /// true if at least one connected socket can read + bool Ready() noexcept; + /// handle all inbound traffic + void Handle(); + /// send all outbound traffic + void Send(); + + void OnRecv(tcp::Socket &) override; + +private: + CLI &cli; + tcp::Pool pool; + +}; + +} + +#endif diff --git a/src/shared/cli.cpp b/src/shared/cli.cpp index 9663cb2..cb46cfc 100644 --- a/src/shared/cli.cpp +++ b/src/shared/cli.cpp @@ -64,6 +64,11 @@ CLI::Command::~Command() { void TeleportCommand::Execute(CLI &, CLIContext &ctx, TokenStreamReader &args) { + if (!ctx.HasPlayer()) { + ctx.Error("teleport needs player to operate on"); + return; + } + glm::vec3 pos(args.GetFloat(), args.GetFloat(), args.GetFloat()); EntityState state = ctx.GetPlayer().GetEntity().GetState(); state.pos = ExactLocation(pos).Sanitize(); diff --git a/src/shared/net.cpp b/src/shared/net.cpp new file mode 100644 index 0000000..17b2e8d --- /dev/null +++ b/src/shared/net.cpp @@ -0,0 +1,121 @@ +#include "CommandService.hpp" + +#include "CLI.hpp" +#include "CommandBuffer.hpp" + +#include +#include + +using namespace std; + + +namespace blank { + +CommandService::CommandService(CLI &cli, unsigned short port) +: cli(cli) +, pool() { + pool.AddConnection(tcp::Socket(port), this); + cout << "listening on TCP port " << port << endl; +} + +CommandService::~CommandService() { + +} + + +void CommandService::Wait(int timeout) noexcept { + pool.Check(timeout); +} + +bool CommandService::Ready() noexcept { + return pool.Check(0); +} + +void CommandService::Handle() { + pool.Receive(); +} + +void CommandService::Send() { + pool.Send(); +} + +void CommandService::OnRecv(tcp::Socket &serv) { + for (tcp::Socket client = serv.Accept(); client; client = serv.Accept()) { + pool.AddConnection(move(client), new CommandBuffer(cli)); + } +} + + +CommandBuffer::CommandBuffer(CLI &cli) +: cli(cli) +, write_buffer() +, read_buffer(1440, '\0') +, head(0) { + +} + +CommandBuffer::~CommandBuffer() { + +} + + +void CommandBuffer::Error(const string &msg) { + write_buffer += " ! "; + write_buffer += msg; + write_buffer += '\n'; +} + +void CommandBuffer::Message(const string &msg) { + write_buffer += " > "; + write_buffer += msg; + write_buffer += '\n'; +} + +void CommandBuffer::Broadcast(const string &msg) { + // TODO: broadcast should be an operation of the + // environment, not the singular context + write_buffer += " @ "; + write_buffer += msg; + write_buffer += '\n'; +} + + +void CommandBuffer::OnSend(tcp::Socket &sock) { + if (write_buffer.empty()) { + return; + } + size_t len = sock.Send(write_buffer.data(), write_buffer.size()); + write_buffer.erase(0, len); +} + +void CommandBuffer::OnRecv(tcp::Socket &sock) { + size_t len = sock.Recv(&read_buffer[0], read_buffer.size() - head); + head += len; + // scan for lines + string::iterator begin = read_buffer.begin(); + string::iterator end = begin + head; + string::iterator handled = begin; + for ( + string::iterator i = find(handled, end, '\n'); + i != end; + i = find(handled, end, '\n') + ) { + string line(handled, i); + cli.Execute(*this, line); + handled = ++i; + } + if (handled == end) { + // guzzled it all + head = 0; + } else if (handled != begin) { + // half a line remaining, move it to the start of the buffer + move(handled, end, begin); + head = distance(handled, end); + } +} + +void CommandBuffer::OnRemove(tcp::Socket &) noexcept { + delete this; +} + +} diff --git a/src/standalone/standalone.cpp b/src/standalone/standalone.cpp index f364aa4..4c502ba 100644 --- a/src/standalone/standalone.cpp +++ b/src/standalone/standalone.cpp @@ -18,7 +18,7 @@ namespace blank { namespace standalone { DirectCLIFeedback::DirectCLIFeedback(Player &p, HUD &h) -: CLIContext(p) +: CLIContext(&p) , hud(h) { }