From 7f7bd5a781f2d0a01a5199f0e6f79adde31fde6a Mon Sep 17 00:00:00 2001 From: jyelon Date: Thu, 22 Feb 2024 17:30:24 -0500 Subject: [PATCH] Client can now connect to servers interactively --- luprex/cpp/core/lpxclient.cpp | 94 ++++++++++++++++++++++++--------- luprex/cpp/core/lpxserver.cpp | 5 ++ luprex/cpp/core/luaconsole.cpp | 6 +++ luprex/cpp/core/printbuffer.cpp | 3 ++ luprex/cpp/core/printbuffer.hpp | 2 + luprex/cpp/core/util.cpp | 37 ++++++++++++- luprex/cpp/core/util.hpp | 3 ++ luprex/cpp/core/world-core.cpp | 8 +-- 8 files changed, 129 insertions(+), 29 deletions(-) diff --git a/luprex/cpp/core/lpxclient.cpp b/luprex/cpp/core/lpxclient.cpp index 1dfdfd58..ae862e46 100644 --- a/luprex/cpp/core/lpxclient.cpp +++ b/luprex/cpp/core/lpxclient.cpp @@ -22,10 +22,13 @@ public: Gui gui_; public: - void set_initial_state() { + void set_initial_state_connect(const eng::string &hostspec) { // Create the world model. world_.reset(new World(WORLD_TYPE_PREDICTIVE)); + // Create the communication channel. + channel_ = new_outgoing_channel(hostspec); + // This is a temporary actor that will be used only until the server sends // us the first difference transmission. We do this only to establish // the invariant that there's always an actor. When the first difference @@ -38,6 +41,34 @@ public: // Export stuff to the graphics engine. set_visible_world_and_actor(world_.get(), actor_id_); + + // Reset the print channeler + print_channeler_.reset(); + } + + void set_initial_state_standalone(std::string_view srcpk) { + // Create the world model. + world_.reset(new World(WORLD_TYPE_MASTER)); + + // Update the source code of the master model. + world_->update_source(srcpk); + + // Make sure the channel is empty. + channel_.reset(); + + // Create the standalone actor. + actor_id_ = world_->create_login_actor(); + + // TODO: initialize the standalone actor. + + // Clear the unack command queue. + unack_.clear(); + + // Export stuff to the graphics engine. + set_visible_world_and_actor(world_.get(), actor_id_); + + // Reset the print channeler + print_channeler_.reset(); } @@ -54,30 +85,31 @@ public: } void world_to_asynchronous() { - if (world_->snapshot_empty()) { - world_->snapshot(); - for (const Invocation &inv : unack_) { - world_->invoke(inv); + if (!world_->is_authoritative()) { + if (world_->snapshot_empty()) { + world_->snapshot(); + for (const Invocation &inv : unack_) { + world_->invoke(inv); + } } } } void abandon_server() { - stdostream() << "Abandoning server." << std::endl; + // When we abandon the server, we leave the world model + // hanging around to preserve the invariant that there + // is always a world model. Then, we trigger a rescan + // of the lua source. When the lua source shows up, then + // we will create a standalone model to replace the client + // model. - // Put the world model back into a known-good state. - set_initial_state(); - - // Disconnect from the server. channel_.reset(); + rescan_lua_source(); } virtual void event_init(std::string_view srcpk, int argc, char *argv[]) { // Put the world into the starting state. - set_initial_state(); - - // Establish a connection to the server. - channel_ = new_outgoing_channel("nocert:localhost:8085"); + set_initial_state_standalone(srcpk); // Set the console prompt set_console_prompt(console_.get_prompt()); @@ -85,15 +117,24 @@ public: void send_invocation(const Invocation &inv) { if (channel_ == nullptr) { - stdostream() << "Cannot invoke any actions, not connected." << std::endl; - return; + if ((!world_->is_authoritative()) && (inv.kind() == Invocation::KIND_LUA_SOURCE)) { + // We have a client model, but no client connection. That means we're + // in the process of shutting down a client model. The client model + // is supposed to linger until the lua source is reread. Once we have + // the lua source, we're supposed to throw out the client model and + // create a standalone model. + set_initial_state_standalone(inv.datapack()); + } else { + world_->invoke(inv); + } + } else { + world_to_asynchronous(); + world_->invoke(inv); + unack_.push_back(inv); + StreamBuffer *sb = channel_->out(); + sb->write_uint8(util::MSG_INVOKE); + inv.serialize(sb); } - world_to_asynchronous(); - world_->invoke(inv); - unack_.push_back(inv); - StreamBuffer *sb = channel_->out(); - sb->write_uint8(util::MSG_INVOKE); - inv.serialize(sb); } void do_luainvoke_command(const StringVec &words) { @@ -149,6 +190,10 @@ public: stop_driver(); } + void do_connect_command(const util::StringVec &words) { + set_initial_state_connect(util::ss("nocert:", words[1], ":8085")); + } + void do_command(const util::StringVec &words) { if (words.empty()) return; else if (words[0] == "luainvoke") do_luainvoke_command(words); @@ -161,6 +206,7 @@ public: else if (words[0] == "cpl") do_cpl_command(words); else if (words[0] == "work") do_work_command(words); else if (words[0] == "quit") do_quit_command(words); + else if (words[0] == "connect") do_connect_command(words); else { stdostream() << "Unsupported command: " << words[0] << std::endl; } @@ -256,9 +302,7 @@ public: // Channel print statements. if (print_channeler_.channel(world_->get_printbuffer(actor_id_), stdostream())) { - if (channel_ != nullptr) { - send_invocation(print_channeler_.invocation(actor_id_)); - } + send_invocation(print_channeler_.invocation(actor_id_)); } } }; diff --git a/luprex/cpp/core/lpxserver.cpp b/luprex/cpp/core/lpxserver.cpp index 568ee887..6a17e704 100644 --- a/luprex/cpp/core/lpxserver.cpp +++ b/luprex/cpp/core/lpxserver.cpp @@ -41,6 +41,8 @@ public: // Create an actor for administrative commands. admin_id_ = master_->create_login_actor(); + // TODO: initialize the admin actor. + // Print out admin ID for debugging purposes. stdostream() << "Admin actor id = " << admin_id_ << std::endl; @@ -196,8 +198,11 @@ public: if (chan->port() == 8085) { Client *client = new Client; client->actor_id_ = master_->create_login_actor(); + // TODO: initialize the login actor on the master. client->channel_ = std::move(chan); client->sync_.reset(new World(WORLD_TYPE_PREDICTIVE)); + // This login actor is never used, it is just to preserve the invariant that + // the client model and the server synchronous model are identical. client->sync_->create_login_actor(); clients_.emplace_back(client); stdostream() << "New client: actor id=" << client->actor_id_ << std::endl; diff --git a/luprex/cpp/core/luaconsole.cpp b/luprex/cpp/core/luaconsole.cpp index 4825f8da..2c200e4f 100644 --- a/luprex/cpp/core/luaconsole.cpp +++ b/luprex/cpp/core/luaconsole.cpp @@ -102,6 +102,12 @@ void LuaConsole::simplify(const StringVec &words) { if (words.size() != 1) { synerr("/aborthttp takes no arguments"); } + } else if (words[0] == "connect") { + if ((words.size() == 2)&&(sv::valid_hostname(words[1]))) { + // OK + } else { + synerr("/connect [hostname]"); + } } else { synerr("unrecognized command"); } diff --git a/luprex/cpp/core/printbuffer.cpp b/luprex/cpp/core/printbuffer.cpp index 70a77070..c1daff88 100644 --- a/luprex/cpp/core/printbuffer.cpp +++ b/luprex/cpp/core/printbuffer.cpp @@ -16,6 +16,9 @@ struct PrintBufferCore : public eng::opnew { // Line number of the first unchecked line in the buffer. All line // numbers including this one and beyond are unchecked. + // "Unchecked" lines are lines in a non-authoritative model + // that haven't been verified through difference transmission. + int first_unchecked_; // Constructor. diff --git a/luprex/cpp/core/printbuffer.hpp b/luprex/cpp/core/printbuffer.hpp index 3d98634a..c39316de 100644 --- a/luprex/cpp/core/printbuffer.hpp +++ b/luprex/cpp/core/printbuffer.hpp @@ -104,6 +104,8 @@ public: int last_line() const; // Get the current first unchecked line. + // "Unchecked" lines are lines in a non-authoritative model + // that haven't been verified through difference transmission. // Guaranteed to be between first_line and last_line inclusive. int first_unchecked() const; diff --git a/luprex/cpp/core/util.cpp b/luprex/cpp/core/util.cpp index b721f1a1..7213ca1e 100644 --- a/luprex/cpp/core/util.cpp +++ b/luprex/cpp/core/util.cpp @@ -55,6 +55,30 @@ bool valid_double(string_view value) { return true; } +bool valid_hostname(string_view value) { + bool start_word = true; + bool last_dash = false; + for (char c : value) { + if (c == '.') { + if (start_word) return false; + if (last_dash) return false; + start_word = true; + last_dash = false; + } else if (c == '-') { + if (start_word) return false; + start_word = false; + last_dash = true; + } else if (ascii_isalnum(c)) { + start_word = false; + last_dash = false; + } else { + return false; + } + } + if (start_word || last_dash) return false; + return true; +} + int64_t to_int64(string_view value, int64_t errval) { int64_t result; const char *p = value.data(); @@ -810,7 +834,18 @@ static std::string_view read_number_x(const char *p, bool plus, bool minus, bool } LuaDefine(unittests_util, "", "some unit tests") { - // test str_to_int64, str_to_double + // Test valid_hostname + LuaAssert(L, sv::valid_hostname("foo123")); + LuaAssert(L, !sv::valid_hostname("foo%123")); + LuaAssert(L, sv::valid_hostname("foo-bar")); + LuaAssert(L, !sv::valid_hostname("-foo")); + LuaAssert(L, !sv::valid_hostname("foo-")); + LuaAssert(L, sv::valid_hostname("foo.bar.baz")); + LuaAssert(L, sv::valid_hostname("foo-bar.baz")); + LuaAssert(L, !sv::valid_hostname("foo-bar-.baz")); + LuaAssert(L, !sv::valid_hostname("foo.-bar-baz")); + + // test str_to_int64, str_to_double LuaAssert(L, sv::to_int64("123") == 123); LuaAssert(L, sv::to_int64("123.4") == INT64_MAX); LuaAssert(L, sv::to_int64("12ab") == INT64_MAX); diff --git a/luprex/cpp/core/util.hpp b/luprex/cpp/core/util.hpp index 8cd193a9..cec2bc1b 100644 --- a/luprex/cpp/core/util.hpp +++ b/luprex/cpp/core/util.hpp @@ -62,6 +62,9 @@ bool valid_double(string_view v); bool valid_int64(string_view v); bool valid_hex64(string_view v); +// Check if a hostname is a valid DNS (ascii) hostname. +bool valid_hostname(string_view v); + // Convert strings to numbers. Returns errval on failure. // // The integer parser accepts a sequence of digits, diff --git a/luprex/cpp/core/world-core.cpp b/luprex/cpp/core/world-core.cpp index 308820b7..e3c99697 100644 --- a/luprex/cpp/core/world-core.cpp +++ b/luprex/cpp/core/world-core.cpp @@ -1083,9 +1083,11 @@ void World::close_lthread_state() { // send the output to std::cerr. if (lthread_prints_ != nullptr) { const eng::string &output = lthread_prints_->str(); - Tangible *actor = tangible_get(lthread_actor_id_); - if (actor != nullptr) { - actor->print_buffer_.add_string(output, is_authoritative()); + if (output.size() > 0) { + Tangible *actor = tangible_get(lthread_actor_id_); + if (actor != nullptr) { + actor->print_buffer_.add_string(output, is_authoritative()); + } } } // Now clean up everything.