diff --git a/luprex/core/cpp/eng-malloc.hpp b/luprex/core/cpp/eng-malloc.hpp index 30afaf53..27a06d78 100644 --- a/luprex/core/cpp/eng-malloc.hpp +++ b/luprex/core/cpp/eng-malloc.hpp @@ -142,6 +142,9 @@ namespace eng { // eng::nevernew. A class containing private operator new and // operator delete, making it impossible to 'new' the class. +// This means the class must be embedded as a field in some other +// class, and it gets allocated when its enclosing object gets +// allocated. namespace eng { class nevernew { private: diff --git a/luprex/core/cpp/http.cpp b/luprex/core/cpp/http.cpp index 5042701c..5db07712 100644 --- a/luprex/core/cpp/http.cpp +++ b/luprex/core/cpp/http.cpp @@ -189,6 +189,9 @@ public: HttpOutRequest::HttpOutRequest() { verify_certificate_ = true; port_ = 0; + request_id_ = 0; + place_id_ = 0; + thread_id_ = 0; } void HttpOutRequest::fail(string_view s) { @@ -395,8 +398,6 @@ void HttpOutRequest::set_config(LuaStack &LS0, LuaSlot tab) { set_port(LS, val); } else if (kstr == "path") { set_path(LS, val); - } else if (kstr == "encodedpath") { - set_path(LS, val); } else if (kstr == "params") { set_params(LS, val); } else if (kstr == "url") { @@ -430,6 +431,7 @@ eng::string HttpOutRequest::check() const { return ""; } + void HttpOutRequest::send_internal(StreamBuffer *sb, bool debug_string) const { // If there's an error in the request, handle it. In debug string mode, // we just put the error into the output. In production mode, we assert @@ -469,18 +471,78 @@ void HttpOutRequest::send_internal(StreamBuffer *sb, bool debug_string) const { sb->write_bytes("User-agent: Mozilla 5.0 (luprex)"); sb->write_bytes(linebreak); + // Add the requester IDs (debug string only) + if (debug_string && ((request_id_ != 0) || (place_id_ != 0) || (thread_id_ != 0))) { + sb->write_bytes("X-Requester-Ids: "); + sb->ostream() << "rid=" << request_id_ << "; pid=" << place_id_ << "; tid=" << thread_id_; + sb->write_bytes(linebreak); + } + // Send the extra linebreak. if (!debug_string) { sb->write_bytes(linebreak); } } +void HttpOutRequest::serialize(StreamBuffer *sb) const { + sb->write_int64(request_id_); + sb->write_int64(place_id_); + sb->write_int64(thread_id_); + sb->write_string(error_); + sb->write_bool(verify_certificate_); + sb->write_string(method_); + sb->write_string(host_); + sb->write_int32(port_); + sb->write_string(path_); + sb->write_int32(params_.size()); + for (const auto &pair : params_) { + sb->write_string(pair.first); + sb->write_string(pair.second); + } +} + +void HttpOutRequest::deserialize(StreamBuffer *sb) { + request_id_ = sb->read_int64(); + place_id_ = sb->read_int64(); + thread_id_ = sb->read_int64(); + error_ = sb->read_string(); + verify_certificate_ = sb->read_bool(); + method_ = sb->read_string(); + host_ = sb->read_string(); + port_ = sb->read_int32(); + path_ = sb->read_string(); + int32_t nparams = sb->read_int32(); + params_.clear(); + for (int i = 0; i < nparams; i++) { + eng::string k = sb->read_string(); + eng::string v = sb->read_string(); + params_[k] = v; + } +} + eng::string HttpOutRequest::DebugString() { StreamBuffer sb; send_internal(&sb, true); return eng::string(sb.view()); } +void HttpOutRequestMap::serialize(StreamBuffer *sb) const { + sb->write_int32(size()); + for (const auto &pair : *this) { + pair.second.serialize(sb); + } +} + +void HttpOutRequestMap::deserialize(StreamBuffer *sb) { + int32_t count = sb->read_int32(); + clear(); + HttpOutRequest req; + for (int i = 0; i < count; i++) { + req.deserialize(sb); + (*this)[req.request_id()] = req; + } +} + HttpInResponse::HttpInResponse() { status_code_ = 0; response_length_ = 0; @@ -513,7 +575,7 @@ void HttpInResponse::fail(int code, string_view message) { void HttpInResponse::incomplete(bool closed) { if (closed) { - fail(500, "response truncated"); + fail(500, "internal server error: response truncated"); } else { fail(0, "response not yet fully received"); } @@ -526,7 +588,7 @@ void HttpInResponse::parse_content_encoding(string_view value) { void HttpInResponse::parse_content_length(string_view value) { int64_t code = sv::to_int64(value); if ((code < 0) || (code > INT_MAX)) { - fail(500, util::ss("unparseable content-length: ", value)); + fail(500, util::ss("internal server error: unparseable content-length: ", value)); } content_length_ = code; } @@ -536,7 +598,7 @@ void HttpInResponse::parse_content_type(string_view value) { string_view ctview(ctype); mime_type_ = sv::trim(sv::read_to_sep(ctview, ';')); if (mime_type_.empty()) { - fail(500, util::ss("unparseable content-type: ", value)); + fail(500, util::ss("internal server error: unparseable content-type: ", value)); return; } while (true) { @@ -571,14 +633,14 @@ void HttpInResponse::parse_header(string_view header, string_view value) { } else if (header == "transfer-encoding") { parse_transfer_encoding(value); } else if (header == "content-range") { - fail(500, util::ss("unsupported response header: ", header)); + fail(406, util::ss("not acceptable: unsupported response header: ", header)); } } bool HttpInResponse::parse_content_basic(std::string_view &view, bool closed) { if (content_length_ >= 0) { if (content_length_ > MAX_CONTENT_LENGTH) { - fail(500, "content too long"); + fail(413, util::ss("payload too large: luprex limit=", MAX_CONTENT_LENGTH)); return false; } if (int(view.size()) < content_length_) { @@ -588,7 +650,7 @@ bool HttpInResponse::parse_content_basic(std::string_view &view, bool closed) { content_ = sv::read_nbytes(view, content_length_); } else { if (int64_t(view.size()) > MAX_CONTENT_LENGTH) { - fail(500, "content too long"); + fail(413, util::ss("payload too large: luprex limit=", MAX_CONTENT_LENGTH)); return false; } if (!closed) { @@ -611,17 +673,17 @@ bool HttpInResponse::parse_content_chunked(std::string_view &view, bool closed) } int64_t chunk_size = sv::to_hex64(chunk_header, -1); if (chunk_size < 0) { - fail(500, "unparseable chunk header"); + fail(500, "internal server error: unparseable chunk header"); return false; } if (chunk_size > MAX_CONTENT_LENGTH) { - fail(500, "content too long"); + fail(413, util::ss("payload too large: luprex limit=", MAX_CONTENT_LENGTH)); return false; } if (chunk_size == 0) break; total_size += chunk_size; if (total_size > MAX_CONTENT_LENGTH) { - fail(500, "content too long"); + fail(413, util::ss("payload too large: luprex limit=", MAX_CONTENT_LENGTH)); return false; } std::string_view chunk = sv::read_nbytes(view, chunk_size); @@ -631,7 +693,7 @@ bool HttpInResponse::parse_content_chunked(std::string_view &view, bool closed) } std::string_view newline = sv::read_to_line(view); if (!newline.empty()) { - fail(500, "corrupted chunk encoding"); + fail(500, "internal server error: corrupted chunk encoding"); return false; } if (sv::isnull(view)) { @@ -665,7 +727,7 @@ void HttpInResponse::parse(const StreamBuffer *sb, bool closed) { string_view scode = sv::read_to_space(status); int64_t code = sv::to_int64(scode, 0); if ((code < 100) || (code > 599)) { - fail(500, util::ss("protocol error: invalid response code: ", scode)); + fail(500, util::ss("internal server error: invalid response code: ", scode)); } status_code_ = code; @@ -690,11 +752,11 @@ void HttpInResponse::parse(const StreamBuffer *sb, bool closed) { } eng::string command = util::ascii_tolower(sv::trim(sv::read_to_sep(header, ':'))); if (sv::isnull(header)) { - fail(500, util::ss("protocol error: no colon in header line: ", command)); + fail(500, util::ss("internal server error: no colon in header line: ", command)); return; } if (!words_separated_by_dashes(command)) { - fail(500, util::ss("protocol error: invalid header: ", command)); + fail(500, util::ss("internal server error: invalid header: ", command)); return; } parse_header(command, sv::trim(header)); @@ -716,7 +778,7 @@ void HttpInResponse::parse(const StreamBuffer *sb, bool closed) { // If it's not a redirect, disallow 'location'. if ((status_code_ < 300) || (status_code_ > 399)) { if (!location_.empty()) { - fail(500, util::ss("redirect specified, but result code not 300-399: ", code)); + fail(500, util::ss("internal server error: location specified, but result code not 300-399: ", code)); return; } } @@ -734,18 +796,18 @@ void HttpInResponse::parse(const StreamBuffer *sb, bool closed) { // If it's multipart, reject it. if (sv::has_prefix(mime_type_, "multipart/")) { - fail(500, "multipart messages not implemented"); + fail(406, "not acceptable: multipart messages not supported"); return; } // If it's text, demand a reasonable charset. Otherwise, // ignore the charset. if (sv::has_prefix(mime_type_, "text/")) { - if (charset_.empty()) { + if (charset_.empty() || (charset_ == "ascii")) { charset_ = "utf-8"; } if (charset_ != "utf-8") { - fail(500, util::ss("charset not supported: ", charset_)); + fail(406, util::ss("not acceptable: charset not supported: ", charset_)); return; } } else { @@ -755,7 +817,7 @@ void HttpInResponse::parse(const StreamBuffer *sb, bool closed) { // Uncompress the content. if ((content_encoding_ == "") || (content_encoding_ == "identity")) { } else { - fail(500, util::ss("content-encoding not supported: ", content_encoding_)); + fail(406, util::ss("not acceptable: content-encoding not supported: ", content_encoding_)); return; } @@ -767,7 +829,7 @@ void HttpInResponse::parse(const StreamBuffer *sb, bool closed) { } } -void HttpInResponse::store(LuaStack &LS0, LuaSlot tab) { +void HttpInResponse::store(LuaStack &LS0, LuaSlot tab) const { LuaStack LS(LS0.state()); LS.newtable(tab); @@ -784,10 +846,24 @@ void HttpInResponse::store(LuaStack &LS0, LuaSlot tab) { } // Debugging fields. Do not use for lua programming. - LS.rawset(tab, "dbg-content-length", content_length_); - LS.rawset(tab, "dbg-transfer-encoding", transfer_encoding_); - LS.rawset(tab, "dbg-charset", charset_); - LS.rawset(tab, "dbg-response-length", response_length_); + if (content_length_ >= 0) { + LS.rawset(tab, "dbg_contentlength", content_length_); + } + if (!transfer_encoding_.empty()) { + LS.rawset(tab, "dbg_transferencoding", transfer_encoding_); + } + if (!charset_.empty()) { + LS.rawset(tab, "dbg_charset", charset_); + } + if (response_length_ != 0) { + LS.rawset(tab, "dbg_responselength", response_length_); + } +} + +void HttpInResponse::store_fail(LuaStack &LS, LuaSlot tab, int status_code, std::string_view error) { + HttpInResponse response; + response.fail(status_code, error); + response.store(LS, tab); } LuaDefine(http_fixurl, "url", "validate URL and repair minor flaws in the URL syntax") { @@ -804,36 +880,35 @@ LuaDefine(http_fixurl, "url", "validate URL and repair minor flaws in the URL sy } -LuaDefine(http_request, "reqtab", - "|Given an HTTP request in the form of a table, returns the same " - "|request as a string, to assist with debugging." +LuaDefine(http_request, "request", + "|Takes an HTTP request in the form of a lua table." + "|The table may contain these fields:" "|" - "|The table can contain:" - "|" - "| method (ie, GET, HEAD, POST, etc)" + "| method (ie, 'GET', 'POST', etc)" "| host (ie, 'google.com')" "| port (default: 443)" - "| url (ie, '/index.html')" + "| path (ie, '/index.html')" "| params (a table of url parameters)" "| verifycertificate (default: true)" + "| url (ie, 'https://host:port/path.html?a=b&c=d')" "|" - "|The url can start with 'https://', or with '/'. If it starts" - "|with 'https://', then the URL includes the host and port, which" - "|then must not be specified separately." + "|You can specify url components separately (host, port, path," + "|and params), or you can specify the entire url as a unit. " + "|If you specify components, they must not be url-encoded. " + "|If you specify the url as a whole, it must already be url-encoded." "|" - "|Note that plain HTTP is not supported - we only allow HTTPS." + "|You can omit the port, in which case it defaults to the" + "|standard https port. You can omit verifycertificate, in which" + "|case it defaults to true. You can omit the method if the" + "|method is implied by the function you called (eg, 'http.get')." + "|" + "|Note that unencrypted http is not supported - we only allow https." "|However, you can talk to a server that has a dummy certificate" "|by specifying verifycertificate=false." "|" - "|This module will automatically url encode everything for you." - "|Therefore, you shouldn't url encode anything, otherwise," - "|you'll end up double-encoding." - "|" - "|You cannot include url parameters as part of the url. If you try," - "|then your ?, &, and = characters will get url encoded, which will" - "|cause them to not function. To use url parameters, you must" - "|use the separate params table." - "|") { + "|This routine, http.request, returns a debug string for the " + "|request. The debug string looks like the actual http headers" + "|that would be sent.") { LuaArg tab; LuaRet str; LuaStack LS(L, tab, str); @@ -849,7 +924,48 @@ LuaDefine(http_request, "reqtab", return LS.result(); } -LuaDefine(http_response, "text", "") { +LuaDefine(http_response, "response", + "|Returns an HTTP response in the form of a lua table." + "|The table will contain these important fields:" + "|" + "| responsecode - 3-digit HTTP response code." + "| error - an error message, or nil if no error." + "| content - on success, the content, as a string." + "| mimetype - on success, the mime type of the content." + "| location - for HTTP redirects, the target url." + "|" + "|If the mimetype is a text mimetype, then the content" + "|is automatically converted to utf-8." + "|" + "|The table may also contain these debugging-only fields." + "|" + "| dbg_transferencoding - If there was a Transfer-Encoding header." + "| dbg_contentlength - If there was a Content-length header." + "| dbg_charset - Original character set for text mime types." + "| dbg_responselength - Total bytes in the response." + "|" + "|None of the dbg fields is needed to understand the response." + "|For example, consider dbg_charset. When text content is" + "|passed to lua, the content is automatically converted to utf-8." + "|So dbg_charset only tells you what the character set used" + "|to be, before it was converted to utf-8." + "|" + "|If an http routine generates an error, that error will be" + "|expressed as a status code. These locally-generated status" + "|codes can be:" + "|" + "| 400 (bad request) - the request was malformed" + "| 503 (service unavailable) - dns fail, connect fail, or ssl fail" + "| 500 (internal server error)- the response contains invalid HTTP" + "| 406 (not acceptable) - the response used a feature we don't support yet" + "| 413 (payload too large) - we refuse to download something so big" + "| 425 (can't resume) - reloaded a save game with a pending request" + "|" + "|In this case, the error message will be the stock" + "|error message, followed by more information." + "|" + "|This routine, http.response, generates a response by parsing" + "|an actual HTTP response string. This is for debugging only.") { LuaArg text; LuaRet tab; LuaStack LS(L, text, tab); diff --git a/luprex/core/cpp/http.hpp b/luprex/core/cpp/http.hpp index 8f274d8f..c315cf49 100644 --- a/luprex/core/cpp/http.hpp +++ b/luprex/core/cpp/http.hpp @@ -24,6 +24,11 @@ using UrlParameters = eng::map; class HttpOutRequest : public eng::nevernew { private: + // Request IDs. + int64_t request_id_; + int64_t place_id_; + int64_t thread_id_; + // If the request contains an error, the error // message is stored here. eng::string error_; @@ -32,38 +37,36 @@ private: // True is the default. bool verify_certificate_; - // Method: GET, HEAD, POST, etc. + // Method: GET, HEAD, POST, etc. Must be all-caps. eng::string method_; - // The hostname. This is used both for DNS lookup, - // and to create an HTTP Host header. + // The hostname. Not yet url-encoded. eng::string host_; // Port number. int port_; - // You may specify either path or encoded_path. - // The path is not url-encoded, and must not include URL parameters. + // The path. Not yet url-encoded. Can not include URL parameters. eng::string path_; - // If params is nonempty, then we will add URL parameters - // to the URL. The contents of the params field should not be - // urlencoded, the urlencoding is done automatically when the - // request is sent. If you specify encoded_path, then the - // params must be empty, because the encoded path already contains - // the params. + // URL parameters to append to the path. Not yet url-encoded. UrlParameters params_; - + private: void fail(std::string_view error); - void send_internal(StreamBuffer *target, bool debug_string) const; + public: // Construct an empty HTTP request. // All of the fields have empty values. HttpOutRequest(); - // Get fields. + // Get the request IDs. + int64_t request_id() const { return request_id_; } + int64_t place_id() const { return place_id_; } + int64_t thread_id() const { return thread_id_; } + + // Get request-related fields. const eng::string &error() const { return error_; } bool verify_certificate() const { return verify_certificate_; } const eng::string &method() const { return method_; } @@ -74,6 +77,11 @@ public: // Get the network target, eg, "cert:host:port" eng::string target() const; + // Set the request IDs. + void set_request_id(int64_t request_id) { request_id_ = request_id; } + void set_place_id(int64_t place_id) { place_id_ = place_id; } + void set_thread_id(int64_t thread_id) { thread_id_ = thread_id; } + // Populate an HTTP request a piece at a time. // If you pass an invalid value, or if the field is // already set, the routine will generate an error message @@ -113,10 +121,20 @@ public: // Put the request into the stream, assuming HTTP/1.1 void send(StreamBuffer *target) const { send_internal(target, false); } + // Serialize and deserialize. + void serialize(StreamBuffer *sb) const; + void deserialize(StreamBuffer *sb); + // Get the request as a debug string. eng::string DebugString(); }; +class HttpOutRequestMap : public eng::map { +public: + void serialize(StreamBuffer *sb) const; + void deserialize(StreamBuffer *sb); +}; + class HttpInResponse { private: // The HTTP response status code. @@ -201,10 +219,13 @@ public: void parse(const StreamBuffer *sb, bool closed); // Convert the HTTP response to a lua table. - void store(LuaStack &LS, LuaSlot tab); + void store(LuaStack &LS, LuaSlot tab) const; // Convert to a debug string. eng::string DebugString() const; + + // Synthesize an error response and store it in lua. + static void store_fail(LuaStack &LS, LuaSlot tab, int status_code, std::string_view error); }; #endif // HTTP_HPP diff --git a/luprex/core/cpp/lpxserver.cpp b/luprex/core/cpp/lpxserver.cpp index 0095ece3..6d5ae362 100644 --- a/luprex/core/cpp/lpxserver.cpp +++ b/luprex/core/cpp/lpxserver.cpp @@ -91,6 +91,10 @@ public: stop_driver(); } + void do_aborthttp_command(const util::StringVec &words) { + master_->abort_all_http_requests(425, "http requests aborted from the server command line"); + } + void do_command(const util::StringVec &words) { if (words.empty()) return; else if (words[0] == "luainvoke") do_luainvoke_command(words); @@ -101,6 +105,7 @@ public: else if (words[0] == "tick") do_tick_command(words); else if (words[0] == "cpl") do_cpl_command(words); else if (words[0] == "quit") do_quit_command(words); + else if (words[0] == "aborthttp") do_aborthttp_command(words); else { stdostream() << "Unsupported command: " << words[0] << std::endl; } diff --git a/luprex/core/cpp/luaconsole.cpp b/luprex/core/cpp/luaconsole.cpp index 5f6204bc..55093169 100644 --- a/luprex/core/cpp/luaconsole.cpp +++ b/luprex/core/cpp/luaconsole.cpp @@ -94,6 +94,10 @@ void LuaConsole::simplify(const StringVec &words) { if (words.size() != 1) { synerr("/work takes no arguments"); } + } else if (words[0] == "aborthttp") { + if (words.size() != 1) { + synerr("/aborthttp takes no arguments"); + } } else { synerr("unrecognized command"); } diff --git a/luprex/core/cpp/world-accessor.cpp b/luprex/core/cpp/world-accessor.cpp index 6bbea6e2..769fbeda 100644 --- a/luprex/core/cpp/world-accessor.cpp +++ b/luprex/core/cpp/world-accessor.cpp @@ -295,6 +295,9 @@ LuaDefine(tangible_scan, "plane,x,y,radius,omit_nowhere", LuaDefine(wait, "nticks", "|Wait the specified number of ticks.") { World *w = World::fetch_global_pointer(L); + w->guard_blockable(L, "wait"); + + // Parse the argument. LuaArg seconds; LuaStack LS(L, seconds); int64_t n = LS.ckinteger(seconds); @@ -302,30 +305,19 @@ LuaDefine(wait, "nticks", luaL_error(L, "Argument to wait must be between 0 and 1000000"); return LS.result(); } - if (!lua_isyieldable(L)) { - // in a probe, wait throws an error. - luaL_error(L, "cannot wait in a probe."); - return LS.result(); - } else if (!w->is_authoritative()) { - // in a nonauth model, yield is converted to nopredict. - lua_yield(L, 0); - return LS.result(); - } else { - // in an authoritative model, wait schedules a continuation. - w->schedule(w->clock_ + n, w->lthread_thread_id_, w->lthread_place_id_); - lua_yield(L, 0); - return LS.result(); - } + + // Schedule a continuation. + w->schedule(w->clock_ + n, w->lthread_thread_id_, w->lthread_place_id_); + return lua_yield(L, 0); } LuaDefine(nopredict, "", "|Stop predictive execution of this thread.") { - if (lua_gettop(L) != 0) { - luaL_error(L, "tangible.nopredict takes no arguments"); - } World *w = World::fetch_global_pointer(L); - if (lua_isyieldable(L) && !w->is_authoritative()) { - return lua_yield(L, 0); + w->guard_nopredict(L, "nopredict"); + + if (lua_gettop(L) != 0) { + luaL_error(L, "nopredict takes no arguments"); } return 0; } @@ -529,4 +521,36 @@ LuaDefine(doc, "function", } (*ostream) << doc; return LS.result(); +} + +LuaDefine(http_get, "request", + "|Make an HTTP GET request. Returns an HTTP response." + "|See doc(http.request) and doc(http.response).") { + World *w = World::fetch_global_pointer(L); + w->guard_blockable(L, "http.get"); + + LuaArg request; + LuaRet response; + LuaStack LS(L, request, response); + HttpOutRequest req; + + // Parse the request and make sure it's valid. + req.set_config(LS, request); + req.set_defaults(); + eng::string error = req.check(); + if (!error.empty()) { + HttpInResponse::store_fail(LS, response, 400, util::ss("bad request: ", error)); + return LS.result(); + } + + // Give the request an ID. + req.set_request_id(w->id_global_pool_.get_one()); + req.set_place_id(w->lthread_place_id_); + req.set_thread_id(w->lthread_thread_id_); + + // Store it in the global request table. + w->http_requests_[req.request_id()] = req; + + // Block. + return lua_yield(L, 0); } \ No newline at end of file diff --git a/luprex/core/cpp/world-core.cpp b/luprex/core/cpp/world-core.cpp index adadb8cb..ee03bb03 100644 --- a/luprex/core/cpp/world-core.cpp +++ b/luprex/core/cpp/world-core.cpp @@ -393,6 +393,66 @@ void World::update_source(const util::LuaSourceVec &source) { assert(stack_is_clear()); } +void World::http_response(int64_t request_id, const HttpInResponse &response) { + // Find the request. + auto iter = http_requests_.find(request_id); + if (iter == http_requests_.end()) { + return; + } + HttpOutRequest request = iter->second; + http_requests_.erase(iter); + + // Get the place and thread as lua objects. + LuaVar tangibles, place, mt, threads, thinfo, thread; + LuaStack LS(state(), tangibles, place, mt, threads, thinfo, thread); + LS.rawget(tangibles, LuaRegistry, "tangibles"); + LS.rawget(place, tangibles, request.place_id()); + if (!LS.istable(place)) { + return; + } + LS.getmetatable(mt, place); + if (!LS.istable(mt)) { + return; + } + LS.rawget(threads, mt, "threads"); + if (!LS.istable(threads)) { + return; + } + LS.rawget(thinfo, threads, request.thread_id()); + if (!LS.istable(thinfo)) { + return; + } + LS.rawget(thread, thinfo, "thread"); + if (!LS.isthread(thread)) { + return; + } + lua_State *CO = LS.ckthread(thread); + + // Push the response onto the awakening thread. + LuaRet responsetable; + LuaStack LSCO(CO, responsetable); + response.store(LSCO, responsetable); + + // Clean up lua stacks. + LSCO.result(); + LS.result(); + assert(stack_is_clear()); + + // Awaken the thread, with its new return value. + schedule(0, request.thread_id(), request.place_id()); + run_scheduled_threads(); +} + +void World::abort_all_http_requests(int status_code, std::string_view error) { + HttpInResponse abortresponse; + abortresponse.fail(status_code, error); + while (true) { + auto iter = http_requests_.begin(); + if (iter == http_requests_.end()) break; + http_response(iter->second.request_id(), abortresponse); + } +} + void World::run_unittests() { assert(stack_is_clear()); source_db_.run_unittests(); @@ -635,6 +695,30 @@ void World::invoke_lua_source(int64_t actor_id, int64_t place_id, const eng::str } } +void World::guard_blockable(lua_State *L, const char *fn) { + if (lthread_thread_id_ == 0) { + // in a probe, http.get throws an error. + luaL_error(L, "cannot %s in a probe", fn); + assert(false); + } + if (!is_authoritative()) { + // in a nonauth model, http.get is converted to nopredict. + lua_yield(L, 0); + luaL_error(L, "unexplained nopredict failure in %s", fn); + assert(false); + } +} + +void World::guard_nopredict(lua_State *L, const char *fn) { + if (lthread_thread_id_ == 0) { + return; + } + if (!is_authoritative()) { + lua_yield(L, 0); + luaL_error(L, "unexplained nopredict failure in %s", fn); + } +} + void World::schedule(int64_t clk, int64_t thid, int64_t plid) { assert(is_authoritative()); thread_sched_.add(clk, thid, plid); @@ -796,6 +880,7 @@ void World::serialize(StreamBuffer *sb) { id_global_pool_.serialize(sb); sb->write_int64(clock_); thread_sched_.serialize(sb); + http_requests_.serialize(sb); sb->write_uint32(tangibles_.size()); for (const auto &p : tangibles_) { sb->write_int64(p.first); @@ -813,6 +898,7 @@ void World::deserialize(StreamBuffer *sb) { id_global_pool_.deserialize(sb); clock_ = sb->read_int64(); thread_sched_.deserialize(sb); + http_requests_.deserialize(sb); // Mark all tangibles for deletion by setting ID to zero. for (const auto &p : tangibles_) { p.second->plane_item_.set_id(0); @@ -837,6 +923,8 @@ void World::deserialize(StreamBuffer *sb) { ++iter; } } + // After a save and load, http requests no longer should exist + abort_all_http_requests(425, "http requests aborted by loading a save game"); assert(stack_is_clear()); } diff --git a/luprex/core/cpp/world.hpp b/luprex/core/cpp/world.hpp index fc9dba33..66ef47a1 100644 --- a/luprex/core/cpp/world.hpp +++ b/luprex/core/cpp/world.hpp @@ -18,6 +18,7 @@ #include "debugcollector.hpp" #include "printbuffer.hpp" #include "sched.hpp" +#include "http.hpp" #include "source.hpp" #include "gui.hpp" #include "luasnap.hpp" @@ -188,6 +189,9 @@ public: // This is the primary dispatcher for all operations that mutate a world model. // To mutate a world model, create an invocation, then invoke it. // + // It is legal to mutate a world model without using 'Invoke', but + // only in authoritative world models. + // void invoke(const Invocation &inv); // Get the PrintBuffer of the actor. @@ -201,6 +205,14 @@ public: void update_source(const util::LuaSourcePtr &source); void update_source(const util::LuaSourceVec &source); + // Supply an HTTP response to an outstanding HTTP request. + void http_response(int64_t request_id, const HttpInResponse &response); + + // Abort all HTTP requests. This is typically used after + // reloading a world from a save-game. The http requests that + // were in progress are long-since dead. + void abort_all_http_requests(int status_code, std::string_view error); + // Run all unit tests. // void run_unittests(); @@ -216,6 +228,10 @@ public: // bool is_authoritative() const { return (world_type_ == util::WORLD_TYPE_MASTER) || (world_type_ == util::WORLD_TYPE_STANDALONE); } + // Get a table showing all outstanding HTTP requests. + // + const HttpOutRequestMap &http_requests() const { return http_requests_; } + // Serialize and deserialize. // void serialize(StreamBuffer *sb); @@ -263,6 +279,16 @@ public: // int64_t alloc_id_predictable(); + // If we're in a probe, generate an error. + // If we're in a nonauthoritative model, do a nopredict yield. + // Otherwise, return. + void guard_blockable(lua_State *L, const char *fn); + + // If we're in a probe, return. + // If we're in a nonauthoritative model, do a nopredict yield. + // Otherwise, return. + void guard_nopredict(lua_State *L, const char *fn); + private: // Add a thread to the scheduler queue. // @@ -478,6 +504,7 @@ private: eng::unordered_map tangibles_; // Current time. + // int64_t clock_; // Thread schedule: must include every thread, except @@ -485,7 +512,13 @@ private: // Schedule thread_sched_; + // Outstanding HTTP requests, indexed by request ID. + // Authoritative models only. + // + HttpOutRequestMap http_requests_; + // Serialized snapshot of world model. + // StreamBuffer snapshot_; // Redirects. @@ -513,6 +546,7 @@ private: friend int lfn_math_randomstate(lua_State *L); friend int lfn_wait(lua_State *L); friend int lfn_nopredict(lua_State *L); + friend int lfn_http_get(lua_State *L); }; using UniqueWorld = std::unique_ptr; diff --git a/luprex/core/lua/login.lua b/luprex/core/lua/login.lua index 04b9bd0a..8b166b6e 100644 --- a/luprex/core/lua/login.lua +++ b/luprex/core/lua/login.lua @@ -42,3 +42,9 @@ function setfoo(n) function buildq() return tangible.build{class="login", x=10, y=0, z=0, plane="nowhere", graphic="what"} end + +function htest() + print("hi") + pprint(http.get{url="https://mit.edu/"}) + print("bye") + end