Machinery for blocking http requests in place, except actual networking

This commit is contained in:
2022-05-03 00:36:11 -04:00
parent d28c588468
commit c41d0522df
9 changed files with 381 additions and 80 deletions

View File

@@ -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:

View File

@@ -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);

View File

@@ -24,6 +24,11 @@ using UrlParameters = eng::map<eng::string, eng::string>;
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<int64_t, HttpOutRequest> {
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

View File

@@ -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;
}

View File

@@ -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");
}

View File

@@ -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;
}
@@ -530,3 +522,35 @@ 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);
}

View File

@@ -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());
}

View File

@@ -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<int64_t, UniqueTangible> 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<World>;

View File

@@ -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