]> git.localhorst.tv Git - blank.git/commitdiff
add TCP based CLI
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Wed, 16 Nov 2016 15:22:58 +0000 (16:22 +0100)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Wed, 16 Nov 2016 15:22:58 +0000 (16:22 +0100)
which currently can't do anything, because the sole
command needs a player ^^

13 files changed:
doc/running
src/app/Config.hpp
src/app/runtime.cpp
src/net/tcp.cpp [new file with mode: 0644]
src/net/tcp.hpp [new file with mode: 0644]
src/server/Server.hpp
src/server/net.cpp
src/shared/CLIContext.hpp
src/shared/CommandBuffer.hpp [new file with mode: 0644]
src/shared/CommandService.hpp [new file with mode: 0644]
src/shared/cli.cpp
src/shared/net.cpp [new file with mode: 0644]
src/standalone/standalone.cpp

index 60dfe031127e545ad9e2e519a90e10ad99a66a5a..426c46324f5ee7f34705b51a61fcc0e8ad0f9c6c 100644 (file)
@@ -87,6 +87,10 @@ Network
 --port <number>
        port number to connection to (client) or listen on (server)
 
+--cmd-port <number>
+       port number to listen on for command connections
+       the default of 0 disables this feature
+
 --player-name <name>
        use given name to identify with the server (client mode)
        default player name is "default"
index 5261a44281334c180b63b0889710c867a54d8c03..d584766b48ad9bbbbf564ddc74d15e3894652a6f 100644 (file)
@@ -30,6 +30,7 @@ struct Config {
 
                std::string host = "localhost";
                std::uint16_t port = 12354;
+               std::uint16_t cmd_port = 0;
 
        } net;
 
index 2481f93a2edcac66feaf5f09f02aff38f30aaf75..75eeec55dd2c3e9c790ad2dfcd510a1c08396ff9 100644 (file)
@@ -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 (file)
index 0000000..f9e039b
--- /dev/null
@@ -0,0 +1,201 @@
+#include "tcp.hpp"
+
+#include "../app/init.hpp"
+
+#include <stdexcept>
+
+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 (file)
index 0000000..5b5f878
--- /dev/null
@@ -0,0 +1,126 @@
+#ifndef BLANK_NET_TCP_HPP_
+#define BLANK_NET_TCP_HPP_
+
+#include <algorithm>
+#include <list>
+#include <SDL_net.h>
+#include <string>
+
+
+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<std::pair<Socket, IOHandler *>>;
+
+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
index 9b89ab1bbc94692a400c7a3a6eda0054ca98a161..f81c9cea205b4918edfbb26e4da3c1357402baa4 100644 (file)
@@ -7,6 +7,7 @@
 #include "../world/WorldManipulator.hpp"
 
 #include <cstdint>
+#include <memory>
 #include <list>
 #include <SDL_net.h>
 
@@ -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<CommandService> cmd_srv;
 
 };
 
index e88ca1fc28b93c577ebb7676a334cbc2b0e05039..f5e974d7e71bcfb9808e08a7d9fc909aa1dbe2d8 100644 (file)
@@ -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 {
index 8de432b7788adb966e0a890e45111e868290b0ca..5308bb2434146aa5d91e14263c3f5830491a0d03 100644 (file)
@@ -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 (file)
index 0000000..316fe6b
--- /dev/null
@@ -0,0 +1,46 @@
+#ifndef BLANK_SHARED_COMMANDBUFFER_HPP_
+#define BLANK_SHARED_COMMANDBUFFER_HPP_
+
+#include "CLIContext.hpp"
+#include "../net/tcp.hpp"
+
+#include <string>
+
+
+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 (file)
index 0000000..38531ed
--- /dev/null
@@ -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
index 9663cb2c4efb192e5e7f0ee36768c84b737db437..cb46cfc264c289fcb73abb9b89caceaefd60af1e 100644 (file)
@@ -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 (file)
index 0000000..17b2e8d
--- /dev/null
@@ -0,0 +1,121 @@
+#include "CommandService.hpp"
+
+#include "CLI.hpp"
+#include "CommandBuffer.hpp"
+
+#include <algorithm>
+#include <iostream>
+
+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;
+}
+
+}
index f364aa4aeeb8b5ad881aa1b6b110fa64cd6aee0b..4c502ba5a9666edb182ebbe9000dea87ff81ec5d 100644 (file)
@@ -18,7 +18,7 @@ namespace blank {
 namespace standalone {
 
 DirectCLIFeedback::DirectCLIFeedback(Player &p, HUD &h)
-: CLIContext(p)
+: CLIContext(&p)
 , hud(h) {
 
 }