docs
This commit is contained in:
@@ -76,15 +76,15 @@ struct TransportLimits {
|
||||
TransportLimits transport_limits()
|
||||
{
|
||||
TransportLimits limits;
|
||||
limits.max_client_connections = config_u64("TRANSPORT_MAX_CLIENT_CONNECTIONS", limits.max_client_connections);
|
||||
limits.max_http_header_bytes = config_u64("TRANSPORT_MAX_HTTP_HEADER_BYTES", limits.max_http_header_bytes);
|
||||
limits.max_http_body_bytes = config_u64("TRANSPORT_MAX_HTTP_BODY_BYTES", limits.max_http_body_bytes);
|
||||
limits.max_websocket_frame_bytes = config_u64("TRANSPORT_MAX_WEBSOCKET_FRAME_BYTES", limits.max_websocket_frame_bytes);
|
||||
limits.max_websocket_message_bytes = config_u64("TRANSPORT_MAX_WEBSOCKET_MESSAGE_BYTES", limits.max_websocket_message_bytes);
|
||||
limits.max_websocket_output_bytes = config_u64("TRANSPORT_MAX_WEBSOCKET_OUTPUT_BYTES", limits.max_websocket_output_bytes);
|
||||
limits.max_response_bytes = config_u64("TRANSPORT_MAX_RESPONSE_BYTES", limits.max_response_bytes);
|
||||
limits.http_request_timeout_seconds = config_f64("TRANSPORT_HTTP_REQUEST_TIMEOUT_SECONDS", limits.http_request_timeout_seconds);
|
||||
limits.connection_idle_timeout_seconds = config_f64("TRANSPORT_CONNECTION_IDLE_TIMEOUT_SECONDS", limits.connection_idle_timeout_seconds);
|
||||
limits.max_client_connections = to_u64(server_state.config["TRANSPORT_MAX_CLIENT_CONNECTIONS"], limits.max_client_connections);
|
||||
limits.max_http_header_bytes = to_u64(server_state.config["TRANSPORT_MAX_HTTP_HEADER_BYTES"], limits.max_http_header_bytes);
|
||||
limits.max_http_body_bytes = to_u64(server_state.config["TRANSPORT_MAX_HTTP_BODY_BYTES"], limits.max_http_body_bytes);
|
||||
limits.max_websocket_frame_bytes = to_u64(server_state.config["TRANSPORT_MAX_WEBSOCKET_FRAME_BYTES"], limits.max_websocket_frame_bytes);
|
||||
limits.max_websocket_message_bytes = to_u64(server_state.config["TRANSPORT_MAX_WEBSOCKET_MESSAGE_BYTES"], limits.max_websocket_message_bytes);
|
||||
limits.max_websocket_output_bytes = to_u64(server_state.config["TRANSPORT_MAX_WEBSOCKET_OUTPUT_BYTES"], limits.max_websocket_output_bytes);
|
||||
limits.max_response_bytes = to_u64(server_state.config["TRANSPORT_MAX_RESPONSE_BYTES"], limits.max_response_bytes);
|
||||
limits.http_request_timeout_seconds = to_f64(server_state.config["TRANSPORT_HTTP_REQUEST_TIMEOUT_SECONDS"], limits.http_request_timeout_seconds);
|
||||
limits.connection_idle_timeout_seconds = to_f64(server_state.config["TRANSPORT_CONNECTION_IDLE_TIMEOUT_SECONDS"], limits.connection_idle_timeout_seconds);
|
||||
return(limits);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
namespace {
|
||||
|
||||
const u64 UCE_UNIT_ABI_VERSION = 6;
|
||||
const u64 UCE_UNIT_ABI_VERSION = 7;
|
||||
|
||||
struct SharedUnitFilesystemState
|
||||
{
|
||||
@@ -46,7 +46,7 @@ bool compiler_jit_compile_on_request_enabled(Request* context)
|
||||
{
|
||||
if(!context || !context->server)
|
||||
return(true);
|
||||
return(config_bool("JIT_COMPILE_ON_REQUEST", true));
|
||||
return(to_bool(context->server->config["JIT_COMPILE_ON_REQUEST"], true));
|
||||
}
|
||||
|
||||
bool compiler_is_u64_string(String value)
|
||||
@@ -150,6 +150,8 @@ bool compiler_failure_retry_deferred(Request* context, SharedUnit* su, const Sha
|
||||
return(false);
|
||||
if(state.source_time == 0)
|
||||
return(false);
|
||||
if(!state.metadata_exists || !state.metadata_parsed || !state.abi_compatible || !state.input_signature_matches)
|
||||
return(false);
|
||||
auto required_failure_inputs_time = std::max({
|
||||
state.source_time,
|
||||
state.setup_template_time,
|
||||
|
||||
+157
-17
@@ -174,6 +174,74 @@ void DValue::each(std::function <void (const DValue& t, String key)> f) const
|
||||
}
|
||||
}
|
||||
|
||||
StringList DValue::keys() const
|
||||
{
|
||||
StringList result;
|
||||
each([&](const DValue& item, String key) {
|
||||
(void)item;
|
||||
if(key != "")
|
||||
result.push_back(key);
|
||||
});
|
||||
return(result);
|
||||
}
|
||||
|
||||
DValue DValue::values() const
|
||||
{
|
||||
DValue result;
|
||||
result.set_array();
|
||||
each([&](const DValue& item, String key) {
|
||||
(void)key;
|
||||
result.push(item);
|
||||
});
|
||||
return(result);
|
||||
}
|
||||
|
||||
DValue DValue::filter(StringList keys) const
|
||||
{
|
||||
DValue result;
|
||||
const DValue& target = deref();
|
||||
for(auto key : keys)
|
||||
{
|
||||
const DValue* item = target.key(key);
|
||||
if(item)
|
||||
result[key] = *item;
|
||||
}
|
||||
return(result);
|
||||
}
|
||||
|
||||
DValue DValue::filter(std::function<bool (const DValue&, String)> f) const
|
||||
{
|
||||
DValue result;
|
||||
bool input_is_list = is_list();
|
||||
if(input_is_list)
|
||||
result.set_array();
|
||||
each([&](const DValue& item, String key) {
|
||||
if(!f(item, key))
|
||||
return;
|
||||
if(key != "" && !input_is_list)
|
||||
result[key] = item;
|
||||
else
|
||||
result.push(item);
|
||||
});
|
||||
return(result);
|
||||
}
|
||||
|
||||
DValue DValue::map(std::function<DValue (const DValue&, String)> f) const
|
||||
{
|
||||
DValue result;
|
||||
bool input_is_list = is_list();
|
||||
if(input_is_list)
|
||||
result.set_array();
|
||||
each([&](const DValue& item, String key) {
|
||||
DValue mapped = f(item, key);
|
||||
if(key != "" && !input_is_list)
|
||||
result[key] = mapped;
|
||||
else
|
||||
result.push(mapped);
|
||||
});
|
||||
return(result);
|
||||
}
|
||||
|
||||
bool DValue::is_array() const
|
||||
{
|
||||
return(deref().type == 'M');
|
||||
@@ -798,8 +866,9 @@ void DValue::clear()
|
||||
namespace {
|
||||
|
||||
const char* UCEB_MAGIC = "UCEB";
|
||||
const u8 UCEB_VERSION = 1;
|
||||
const u8 UCEB_VERSION = 2;
|
||||
const u8 UCEB_FLAG_LIST = 1;
|
||||
const u32 UCEB_MAX_NESTING_DEPTH = 64;
|
||||
|
||||
thread_local String uce_dv_last_error_text;
|
||||
thread_local String uce_dv_value_result;
|
||||
@@ -831,6 +900,23 @@ bool ucb_read_varint(const String& src, size_t& offset, u64& value_out)
|
||||
return(false);
|
||||
}
|
||||
|
||||
char ucb_node_type(const DValue& value)
|
||||
{
|
||||
const DValue& target = value.deref();
|
||||
switch(target.type)
|
||||
{
|
||||
case('M'):
|
||||
case('S'):
|
||||
case('F'):
|
||||
case('B'):
|
||||
return(target.type);
|
||||
default:
|
||||
// Raw pointers/references are not meaningful across the native/wasm
|
||||
// membrane; preserve the historical wire behavior as an empty scalar.
|
||||
return('S');
|
||||
}
|
||||
}
|
||||
|
||||
String ucb_node_scalar(const DValue& value)
|
||||
{
|
||||
const DValue& target = value.deref();
|
||||
@@ -845,7 +931,7 @@ String ucb_node_scalar(const DValue& value)
|
||||
return(out.str());
|
||||
}
|
||||
case('B'):
|
||||
return(target._bool ? "(true)" : "(false)");
|
||||
return(target._bool ? "1" : "0");
|
||||
case('P'):
|
||||
return("");
|
||||
default:
|
||||
@@ -853,11 +939,50 @@ String ucb_node_scalar(const DValue& value)
|
||||
}
|
||||
}
|
||||
|
||||
bool ucb_decode_scalar(char node_type, const String& scalar, DValue& out, String& error)
|
||||
{
|
||||
switch(node_type)
|
||||
{
|
||||
case('S'):
|
||||
out = scalar;
|
||||
return(true);
|
||||
case('F'):
|
||||
{
|
||||
const char* begin = scalar.c_str();
|
||||
char* end = 0;
|
||||
f64 value = strtod(begin, &end);
|
||||
if(end == begin || end != begin + scalar.size() || !std::isfinite(value))
|
||||
{
|
||||
error = "invalid UCEB2 f64 scalar";
|
||||
return(false);
|
||||
}
|
||||
out = value;
|
||||
return(true);
|
||||
}
|
||||
case('B'):
|
||||
if(scalar == "1" || scalar == "true" || scalar == "(true)")
|
||||
{
|
||||
out.set_bool(true);
|
||||
return(true);
|
||||
}
|
||||
if(scalar == "0" || scalar == "false" || scalar == "(false)")
|
||||
{
|
||||
out.set_bool(false);
|
||||
return(true);
|
||||
}
|
||||
error = "invalid UCEB2 bool scalar";
|
||||
return(false);
|
||||
}
|
||||
error = "invalid UCEB2 scalar type tag";
|
||||
return(false);
|
||||
}
|
||||
|
||||
void ucb_encode_node(String& out, const DValue& value)
|
||||
{
|
||||
const DValue& target = value.deref();
|
||||
u8 flags = target.is_list() ? UCEB_FLAG_LIST : 0;
|
||||
out.push_back((char)flags);
|
||||
out.push_back(ucb_node_type(target));
|
||||
String scalar = ucb_node_scalar(target);
|
||||
ucb_append_varint(out, scalar.size());
|
||||
out.append(scalar.data(), scalar.size());
|
||||
@@ -877,26 +1002,32 @@ void ucb_encode_node(String& out, const DValue& value)
|
||||
|
||||
bool ucb_decode_node(const String& src, size_t& offset, DValue& out, String& error, u32 depth = 0)
|
||||
{
|
||||
if(depth > 1024)
|
||||
if(depth >= UCEB_MAX_NESTING_DEPTH)
|
||||
{
|
||||
error = "UCEB1 nesting limit exceeded";
|
||||
error = "UCEB2 nesting limit exceeded";
|
||||
return(false);
|
||||
}
|
||||
if(offset >= src.size())
|
||||
if(offset > src.size() || src.size() - offset < 2)
|
||||
{
|
||||
error = "unexpected end of UCEB1 node";
|
||||
error = "unexpected end of UCEB2 node";
|
||||
return(false);
|
||||
}
|
||||
u8 flags = (u8)src[offset++];
|
||||
char node_type = src[offset++];
|
||||
if(node_type != 'M' && node_type != 'S' && node_type != 'F' && node_type != 'B')
|
||||
{
|
||||
error = "invalid UCEB2 node type tag";
|
||||
return(false);
|
||||
}
|
||||
u64 scalar_len = 0;
|
||||
if(!ucb_read_varint(src, offset, scalar_len))
|
||||
{
|
||||
error = "invalid UCEB1 scalar length";
|
||||
error = "invalid UCEB2 scalar length";
|
||||
return(false);
|
||||
}
|
||||
if(scalar_len > src.size() - offset)
|
||||
if(offset > src.size() || scalar_len > src.size() - offset)
|
||||
{
|
||||
error = "UCEB1 scalar length exceeds input";
|
||||
error = "UCEB2 scalar length exceeds input";
|
||||
return(false);
|
||||
}
|
||||
String scalar(src.data() + offset, (size_t)scalar_len);
|
||||
@@ -905,15 +1036,19 @@ bool ucb_decode_node(const String& src, size_t& offset, DValue& out, String& err
|
||||
u64 child_count = 0;
|
||||
if(!ucb_read_varint(src, offset, child_count))
|
||||
{
|
||||
error = "invalid UCEB1 child count";
|
||||
error = "invalid UCEB2 child count";
|
||||
return(false);
|
||||
}
|
||||
|
||||
out.clear();
|
||||
if(child_count == 0 && (flags & UCEB_FLAG_LIST) == 0)
|
||||
if(node_type != 'M')
|
||||
{
|
||||
out = scalar;
|
||||
return(true);
|
||||
if(child_count != 0 || (flags & UCEB_FLAG_LIST) != 0)
|
||||
{
|
||||
error = "UCEB2 scalar node cannot have children or list flag";
|
||||
return(false);
|
||||
}
|
||||
return(ucb_decode_scalar(node_type, scalar, out, error));
|
||||
}
|
||||
if((flags & UCEB_FLAG_LIST) != 0)
|
||||
out.set_array();
|
||||
@@ -923,16 +1058,21 @@ bool ucb_decode_node(const String& src, size_t& offset, DValue& out, String& err
|
||||
u64 key_len = 0;
|
||||
if(!ucb_read_varint(src, offset, key_len))
|
||||
{
|
||||
error = "invalid UCEB1 child key length";
|
||||
error = "invalid UCEB2 child key length";
|
||||
return(false);
|
||||
}
|
||||
if(key_len > src.size() - offset)
|
||||
if(offset > src.size() || key_len > src.size() - offset)
|
||||
{
|
||||
error = "UCEB1 child key length exceeds input";
|
||||
error = "UCEB2 child key length exceeds input";
|
||||
return(false);
|
||||
}
|
||||
String key(src.data() + offset, (size_t)key_len);
|
||||
offset += (size_t)key_len;
|
||||
if(depth + 1 >= UCEB_MAX_NESTING_DEPTH)
|
||||
{
|
||||
error = "UCEB2 nesting limit exceeded";
|
||||
return(false);
|
||||
}
|
||||
DValue child;
|
||||
if(!ucb_decode_node(src, offset, child, error, depth + 1))
|
||||
return(false);
|
||||
@@ -992,7 +1132,7 @@ bool ucb_decode(const String& encoded, DValue& out, String* error_out)
|
||||
return(true);
|
||||
}
|
||||
if(error == "")
|
||||
error = "trailing bytes after UCEB1 document";
|
||||
error = "trailing bytes after UCEB2 document";
|
||||
}
|
||||
if(error_out)
|
||||
*error_out = error;
|
||||
|
||||
@@ -23,6 +23,11 @@ struct DValue {
|
||||
// conversions take an optional default that is returned when the value is
|
||||
// missing (empty) or cannot be converted to the requested type.
|
||||
void each(std::function <void (const DValue& t, String key)> f) const;
|
||||
StringList keys() const;
|
||||
DValue values() const;
|
||||
DValue filter(StringList keys) const;
|
||||
DValue filter(std::function<bool (const DValue&, String)> f) const;
|
||||
DValue map(std::function<DValue (const DValue&, String)> f) const;
|
||||
bool is_array() const;
|
||||
bool is_list() const;
|
||||
String to_string(String default_value = "") const;
|
||||
|
||||
+157
-98
@@ -112,95 +112,6 @@ String list_find(StringList items, std::function<bool (String)> f, String fallba
|
||||
return(fallback);
|
||||
}
|
||||
|
||||
StringList dv_keys(DValue tree)
|
||||
{
|
||||
StringList result;
|
||||
tree.each([&](const DValue& item, String key) {
|
||||
if(key != "")
|
||||
result.push_back(key);
|
||||
});
|
||||
return(result);
|
||||
}
|
||||
|
||||
DValue dv_values(DValue tree)
|
||||
{
|
||||
DValue result;
|
||||
result.set_array();
|
||||
tree.each([&](const DValue& item, String key) {
|
||||
result.push(item);
|
||||
});
|
||||
return(result);
|
||||
}
|
||||
|
||||
DValue dv_pick(DValue tree, StringList keys)
|
||||
{
|
||||
DValue result;
|
||||
for(auto key : keys)
|
||||
{
|
||||
DValue* item = tree.key(key);
|
||||
if(item)
|
||||
result[key] = *item;
|
||||
}
|
||||
return(result);
|
||||
}
|
||||
|
||||
DValue dv_omit(DValue tree, StringList keys)
|
||||
{
|
||||
DValue result;
|
||||
std::set<String> omitted(keys.begin(), keys.end());
|
||||
tree.each([&](const DValue& item, String key) {
|
||||
if(key != "" && omitted.find(key) == omitted.end())
|
||||
result[key] = item;
|
||||
});
|
||||
return(result);
|
||||
}
|
||||
|
||||
DValue dv_map(DValue tree, std::function<DValue (const DValue&, String)> f)
|
||||
{
|
||||
DValue result;
|
||||
bool input_is_list = tree.is_list();
|
||||
if(input_is_list)
|
||||
result.set_array();
|
||||
tree.each([&](const DValue& item, String key) {
|
||||
DValue mapped = f(item, key);
|
||||
if(key != "" && !input_is_list)
|
||||
result[key] = mapped;
|
||||
else
|
||||
result.push(mapped);
|
||||
});
|
||||
return(result);
|
||||
}
|
||||
|
||||
DValue dv_filter(DValue tree, std::function<bool (const DValue&, String)> f)
|
||||
{
|
||||
DValue result;
|
||||
bool input_is_list = tree.is_list();
|
||||
if(input_is_list)
|
||||
result.set_array();
|
||||
tree.each([&](const DValue& item, String key) {
|
||||
if(!f(item, key))
|
||||
return;
|
||||
if(key != "" && !input_is_list)
|
||||
result[key] = item;
|
||||
else
|
||||
result.push(item);
|
||||
});
|
||||
return(result);
|
||||
}
|
||||
|
||||
DValue dv_group_by(DValue tree, std::function<String (const DValue&, String)> f)
|
||||
{
|
||||
DValue result;
|
||||
tree.each([&](const DValue& item, String key) {
|
||||
String group = f(item, key);
|
||||
DValue* group_items = result.get_or_create(group);
|
||||
if(!group_items->is_array())
|
||||
group_items->set_array();
|
||||
group_items->push(item);
|
||||
});
|
||||
return(result);
|
||||
}
|
||||
|
||||
String substr(String s, s64 start_pos)
|
||||
{
|
||||
s64 len = s.length();
|
||||
@@ -643,7 +554,7 @@ StringList regex_split(String pattern, String subject, String flags)
|
||||
#else
|
||||
|
||||
// PCRE2 is not compiled into the wasm core; regex runs host-side (the host
|
||||
// already links libpcre2). One UCEB1-marshalled hostcall carries the request
|
||||
// already links libpcre2). One UCEB2-marshalled hostcall carries the request
|
||||
// {op,pattern,subject,flags,replacement} in and the result tree out — the host
|
||||
// runs the native regex_* and packs the answer. See uce_host_regex in
|
||||
// src/wasm/worker.cpp.
|
||||
@@ -1063,6 +974,48 @@ f64 float_val(String s)
|
||||
return(strtod(s.c_str(), 0));
|
||||
}
|
||||
|
||||
u64 to_u64(String s, u64 fallback)
|
||||
{
|
||||
String raw = trim(s);
|
||||
if(raw == "")
|
||||
return(fallback);
|
||||
char* end = 0;
|
||||
unsigned long long value = strtoull(raw.c_str(), &end, 10);
|
||||
return(end && *end == 0 ? (u64)value : fallback);
|
||||
}
|
||||
|
||||
s64 to_s64(String s, s64 fallback)
|
||||
{
|
||||
String raw = trim(s);
|
||||
if(raw == "")
|
||||
return(fallback);
|
||||
char* end = 0;
|
||||
long long value = strtoll(raw.c_str(), &end, 10);
|
||||
return(end && *end == 0 ? (s64)value : fallback);
|
||||
}
|
||||
|
||||
f64 to_f64(String s, f64 fallback)
|
||||
{
|
||||
String raw = trim(s);
|
||||
if(raw == "")
|
||||
return(fallback);
|
||||
char* end = 0;
|
||||
double value = strtod(raw.c_str(), &end);
|
||||
return(end && *end == 0 ? (f64)value : fallback);
|
||||
}
|
||||
|
||||
bool to_bool(String s, bool fallback)
|
||||
{
|
||||
String raw = trim(to_lower(s));
|
||||
if(raw == "")
|
||||
return(fallback);
|
||||
if(raw == "1" || raw == "true" || raw == "yes" || raw == "on")
|
||||
return(true);
|
||||
if(raw == "0" || raw == "false" || raw == "no" || raw == "off")
|
||||
return(false);
|
||||
return(fallback);
|
||||
}
|
||||
|
||||
String nibble(String& haystack, String delim)
|
||||
{
|
||||
auto idx = haystack.find(delim);
|
||||
@@ -2255,6 +2208,58 @@ DValue yaml_decode(String s)
|
||||
return(parser.parse_document());
|
||||
}
|
||||
|
||||
static bool json_hex_to_u32(String s, u32 start, u32& value)
|
||||
{
|
||||
value = 0;
|
||||
if(start > s.length() || s.length() - start < 4)
|
||||
return(false);
|
||||
for(u32 i = 0; i < 4; i += 1)
|
||||
{
|
||||
char c = s[start + i];
|
||||
if(c >= '0' && c <= '9')
|
||||
value = (value << 4) | (c - '0');
|
||||
else if(c >= 'a' && c <= 'f')
|
||||
value = (value << 4) | (c - 'a' + 10);
|
||||
else if(c >= 'A' && c <= 'F')
|
||||
value = (value << 4) | (c - 'A' + 10);
|
||||
else
|
||||
return(false);
|
||||
}
|
||||
return(true);
|
||||
}
|
||||
|
||||
static bool json_decode_unicode_escape(String s, u32& i, String& result)
|
||||
{
|
||||
u32 codepoint = 0;
|
||||
if(i >= s.length() || s[i] != 'u' || !json_hex_to_u32(s, i + 1, codepoint))
|
||||
return(false);
|
||||
|
||||
u32 next_sequence_start = i + 5;
|
||||
if(codepoint >= 0xd800 && codepoint <= 0xdbff)
|
||||
{
|
||||
u32 low = 0;
|
||||
if(next_sequence_start <= s.length() && s.length() - next_sequence_start >= 6 && s[next_sequence_start] == '\\' && s[next_sequence_start + 1] == 'u' && json_hex_to_u32(s, next_sequence_start + 2, low) && low >= 0xdc00 && low <= 0xdfff)
|
||||
{
|
||||
codepoint = 0x10000 + (((codepoint - 0xd800) << 10) | (low - 0xdc00));
|
||||
String utf8 = xml_utf8_from_codepoint(codepoint);
|
||||
if(utf8 != "")
|
||||
result += utf8;
|
||||
i += 10;
|
||||
return(true);
|
||||
}
|
||||
return(false);
|
||||
}
|
||||
if(codepoint >= 0xdc00 && codepoint <= 0xdfff)
|
||||
return(false);
|
||||
|
||||
String utf8 = xml_utf8_from_codepoint(codepoint);
|
||||
if(utf8 == "")
|
||||
return(false);
|
||||
result += utf8;
|
||||
i += 4;
|
||||
return(true);
|
||||
}
|
||||
|
||||
// https://i.stack.imgur.com/SHLOB.gif
|
||||
String json_decode_String(String s, u32& i, char termination_char)
|
||||
{
|
||||
@@ -2271,6 +2276,9 @@ String json_decode_String(String s, u32& i, char termination_char)
|
||||
}
|
||||
else if(c == '\\')
|
||||
{
|
||||
if(i + 1 >= s.length())
|
||||
return(result);
|
||||
|
||||
i += 1;
|
||||
c = s[i];
|
||||
switch(c)
|
||||
@@ -2293,9 +2301,16 @@ String json_decode_String(String s, u32& i, char termination_char)
|
||||
case('f'):
|
||||
result.append(1, '\f');
|
||||
break;
|
||||
case('u'):
|
||||
// todo decode
|
||||
case('"'):
|
||||
result.append(1, '"');
|
||||
break;
|
||||
case('\''):
|
||||
result.append(1, '\'');
|
||||
break;
|
||||
case('u'):
|
||||
if(i <= s.length() && s.length() - i >= 5 && json_decode_unicode_escape(s, i, result))
|
||||
break;
|
||||
return(result);
|
||||
default:
|
||||
result.append(1, c);
|
||||
break;
|
||||
@@ -2342,19 +2357,58 @@ String json_decode_keyword(String s, u32& i)
|
||||
String json_decode_number(String s, u32& i)
|
||||
{
|
||||
String result;
|
||||
bool saw_dot = false;
|
||||
bool saw_exponent = false;
|
||||
|
||||
json_consume_space(s, i);
|
||||
if(i < s.length() && s[i] == '-')
|
||||
{
|
||||
result.append(1, '-');
|
||||
i += 1;
|
||||
}
|
||||
if(i >= s.length() || !isdigit(s[i]))
|
||||
return("");
|
||||
|
||||
while(i < s.length())
|
||||
{
|
||||
char c = s[i];
|
||||
if(isdigit(c) || c == '.')
|
||||
if(isdigit(c))
|
||||
{
|
||||
result.append(1, c);
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
else
|
||||
if(c == '.' && !saw_dot && !saw_exponent)
|
||||
{
|
||||
return(result);
|
||||
result.append(1, c);
|
||||
saw_dot = true;
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
i += 1;
|
||||
if((c == 'e' || c == 'E') && !saw_exponent)
|
||||
{
|
||||
if(i + 1 >= s.length())
|
||||
break;
|
||||
u32 exponent_start = i;
|
||||
u32 next = i + 1;
|
||||
if(s[next] == '+' || s[next] == '-')
|
||||
next += 1;
|
||||
if(next < s.length() && isdigit(s[next]))
|
||||
{
|
||||
result.append(1, s[i]);
|
||||
i = i + 1;
|
||||
if(i < s.length() && (s[i] == '+' || s[i] == '-'))
|
||||
{
|
||||
result.append(1, s[i]);
|
||||
i += 1;
|
||||
}
|
||||
saw_exponent = true;
|
||||
continue;
|
||||
}
|
||||
i = exponent_start;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return(result);
|
||||
}
|
||||
@@ -2364,6 +2418,9 @@ DValue json_decode_value(String s, u32& i)
|
||||
DValue result;
|
||||
String value = "";
|
||||
json_consume_space(s, i);
|
||||
if(i >= s.length())
|
||||
return(result);
|
||||
|
||||
char c = s[i];
|
||||
//print("json_decode_value " + s.substr(i) + "\n");
|
||||
if(c == '"' || c == '\'') // String value
|
||||
@@ -2373,7 +2430,7 @@ DValue json_decode_value(String s, u32& i)
|
||||
result._String = json_decode_String(s, i, s[i-1]);
|
||||
return(result);
|
||||
}
|
||||
else if(isdigit(c))
|
||||
else if(c == '-' || isdigit(c))
|
||||
{
|
||||
result.type = 'S';
|
||||
result._String = json_decode_number(s, i);
|
||||
@@ -2426,9 +2483,11 @@ DValue json_decode_map(String s, u32& i)
|
||||
else if(c == '"' || c == '\'')
|
||||
{
|
||||
i += 1;
|
||||
if(i >= s.length())
|
||||
return(result);
|
||||
key = json_decode_String(s, i, s[i-1]);
|
||||
json_consume_space(s, i);
|
||||
if(s[i] != ':')
|
||||
if(i >= s.length() || s[i] != ':')
|
||||
return(result); // malformed
|
||||
i += 1;
|
||||
DValue v = json_decode_value(s, i);
|
||||
|
||||
@@ -5,6 +5,10 @@ u8 char_to_u8(char input);
|
||||
u8 hex_to_u8(String src);
|
||||
u64 int_val(String s, u32 base = 10);
|
||||
f64 float_val(String s);
|
||||
u64 to_u64(String s, u64 fallback = 0);
|
||||
s64 to_s64(String s, s64 fallback = 0);
|
||||
f64 to_f64(String s, f64 fallback = 0);
|
||||
bool to_bool(String s, bool fallback = false);
|
||||
String to_lower(String s);
|
||||
String to_upper(String s);
|
||||
String substr(String s, s64 start_pos);
|
||||
@@ -95,13 +99,6 @@ StringList list_sort(StringList items);
|
||||
bool list_some(StringList items, std::function<bool (String)> f);
|
||||
bool list_every(StringList items, std::function<bool (String)> f);
|
||||
String list_find(StringList items, std::function<bool (String)> f, String fallback = "");
|
||||
StringList dv_keys(DValue tree);
|
||||
DValue dv_values(DValue tree);
|
||||
DValue dv_pick(DValue tree, StringList keys);
|
||||
DValue dv_omit(DValue tree, StringList keys);
|
||||
DValue dv_map(DValue tree, std::function<DValue (const DValue&, String)> f);
|
||||
DValue dv_filter(DValue tree, std::function<bool (const DValue&, String)> f);
|
||||
DValue dv_group_by(DValue tree, std::function<String (const DValue&, String)> f);
|
||||
|
||||
template <class ...Args>
|
||||
inline String first(Args... args)
|
||||
|
||||
+1
-61
@@ -193,13 +193,6 @@ StringList ls(String dir)
|
||||
return(StringList());
|
||||
return(split(listing, "\n"));
|
||||
}
|
||||
u64 config_map_u64(StringMap& cfg, String key, u64 fallback) { String raw = first(cfg[key], std::to_string(fallback)); char* end = 0; unsigned long long v = strtoull(raw.c_str(), &end, 10); return(end && *end == 0 ? (u64)v : fallback); }
|
||||
f64 config_map_f64(StringMap& cfg, String key, f64 fallback) { String raw = first(cfg[key], std::to_string(fallback)); char* end = 0; double v = strtod(raw.c_str(), &end); return(end && *end == 0 ? (f64)v : fallback); }
|
||||
bool config_bool_value(String raw, bool fallback) { if(raw == "") return(fallback); return(raw != "0" && raw != "false" && raw != "no" && raw != "off"); }
|
||||
bool config_map_bool(StringMap& cfg, String key, bool fallback) { return(config_bool_value(cfg[key], fallback)); }
|
||||
u64 config_u64(String key, u64 fallback) { return(context ? config_map_u64(context->server->config, key, fallback) : fallback); }
|
||||
f64 config_f64(String key, f64 fallback) { return(context ? config_map_f64(context->server->config, key, fallback) : fallback); }
|
||||
bool config_bool(String key, bool fallback) { return(context ? config_map_bool(context->server->config, key, fallback) : fallback); }
|
||||
DValue request_perf()
|
||||
{
|
||||
size_t required = uce_host_request_perf("", 0, 0, 0);
|
||||
@@ -1412,60 +1405,6 @@ StringList ls(String dir)
|
||||
return(split(trim(shell_exec("ls -1 "+shell_escape(dir))), "\n"));
|
||||
}
|
||||
|
||||
u64 config_map_u64(StringMap& cfg, String key, u64 fallback)
|
||||
{
|
||||
String raw = trim(cfg[key]);
|
||||
if(raw == "")
|
||||
return(fallback);
|
||||
return(int_val(raw));
|
||||
}
|
||||
|
||||
f64 config_map_f64(StringMap& cfg, String key, f64 fallback)
|
||||
{
|
||||
String raw = trim(cfg[key]);
|
||||
if(raw == "")
|
||||
return(fallback);
|
||||
return(float_val(raw));
|
||||
}
|
||||
|
||||
bool config_bool_value(String raw, bool fallback)
|
||||
{
|
||||
raw = trim(to_lower(raw));
|
||||
if(raw == "")
|
||||
return(fallback);
|
||||
if(raw == "1" || raw == "true" || raw == "yes" || raw == "on")
|
||||
return(true);
|
||||
if(raw == "0" || raw == "false" || raw == "no" || raw == "off")
|
||||
return(false);
|
||||
return(fallback);
|
||||
}
|
||||
|
||||
bool config_map_bool(StringMap& cfg, String key, bool fallback)
|
||||
{
|
||||
return(config_bool_value(cfg[key], fallback));
|
||||
}
|
||||
|
||||
u64 config_u64(String key, u64 fallback)
|
||||
{
|
||||
if(!context || !context->server)
|
||||
return(fallback);
|
||||
return(config_map_u64(context->server->config, key, fallback));
|
||||
}
|
||||
|
||||
f64 config_f64(String key, f64 fallback)
|
||||
{
|
||||
if(!context || !context->server)
|
||||
return(fallback);
|
||||
return(config_map_f64(context->server->config, key, fallback));
|
||||
}
|
||||
|
||||
bool config_bool(String key, bool fallback)
|
||||
{
|
||||
if(!context || !context->server)
|
||||
return(fallback);
|
||||
return(config_map_bool(context->server->config, key, fallback));
|
||||
}
|
||||
|
||||
StringMap make_server_settings()
|
||||
{
|
||||
StringMap cfg;
|
||||
@@ -1485,6 +1424,7 @@ StringMap make_server_settings()
|
||||
// Command socket the WS broker listens on; workers flush ws_* dispatch
|
||||
// command batches here at workspace teardown.
|
||||
cfg["WS_BROKER_SOCKET_PATH"] = "/run/uce/ws-broker.sock";
|
||||
cfg["WS_BROKER_OUTBOUND_TIMEOUT_SECONDS"] = "30";
|
||||
cfg["TMP_UPLOAD_PATH"] = "/tmp/uce/uploads";
|
||||
cfg["SESSION_PATH"] = "/tmp/uce/sessions";
|
||||
cfg["COMPILER_SYS_PATH"] = ".";
|
||||
|
||||
@@ -56,14 +56,6 @@ time_t file_mtime(String file_name);
|
||||
void file_unlink(String file_name);
|
||||
String expand_path(String path, String relative_to_path = "");
|
||||
StringList ls(String dir);
|
||||
u64 config_map_u64(StringMap& cfg, String key, u64 fallback);
|
||||
f64 config_map_f64(StringMap& cfg, String key, f64 fallback);
|
||||
bool config_bool_value(String raw, bool fallback = true);
|
||||
bool config_map_bool(StringMap& cfg, String key, bool fallback = true);
|
||||
u64 config_u64(String key, u64 fallback);
|
||||
f64 config_f64(String key, f64 fallback);
|
||||
bool config_bool(String key, bool fallback = true);
|
||||
|
||||
f64 time_precise();
|
||||
u64 time();
|
||||
String time_format_local(String format = "", u64 timestamp = 0);
|
||||
|
||||
+54
-1
@@ -8,6 +8,7 @@
|
||||
#include <sstream>
|
||||
#include <atomic>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
|
||||
typedef unsigned char u8;
|
||||
typedef signed char s8;
|
||||
@@ -51,7 +52,58 @@ inline String operator+(String lhs, f32 rhs) {
|
||||
#define DEBUG_MEMORY_OFF
|
||||
|
||||
typedef std::map<String, String> StringMap;
|
||||
typedef std::vector<String> StringList;
|
||||
|
||||
struct StringList : std::vector<String> {
|
||||
using std::vector<String>::vector;
|
||||
using std::vector<String>::operator=;
|
||||
|
||||
StringList() = default;
|
||||
StringList(const StringList&) = default;
|
||||
StringList(StringList&&) = default;
|
||||
StringList& operator=(const StringList&) = default;
|
||||
StringList& operator=(StringList&&) = default;
|
||||
StringList(const std::vector<String>& source) : std::vector<String>(source) {}
|
||||
StringList(std::vector<String>&& source) : std::vector<String>(std::move(source)) {}
|
||||
StringList& operator=(const std::vector<String>& source) { std::vector<String>::operator=(source); return(*this); }
|
||||
StringList& operator=(std::vector<String>&& source) { std::vector<String>::operator=(std::move(source)); return(*this); }
|
||||
|
||||
template<typename F>
|
||||
StringList filter(F f) const
|
||||
{
|
||||
StringList result;
|
||||
for(const auto& item : *this)
|
||||
{
|
||||
if(f(item))
|
||||
result.push_back(item);
|
||||
}
|
||||
return(result);
|
||||
}
|
||||
|
||||
template<typename F>
|
||||
StringList map(F f) const
|
||||
{
|
||||
StringList result;
|
||||
for(const auto& item : *this)
|
||||
result.push_back(f(item));
|
||||
return(result);
|
||||
}
|
||||
|
||||
StringList keys() const
|
||||
{
|
||||
StringList result;
|
||||
for(size_t i = 0; i < size(); i++)
|
||||
result.push_back(std::to_string(i));
|
||||
return(result);
|
||||
}
|
||||
|
||||
template<typename F>
|
||||
void each(F f) const
|
||||
{
|
||||
for(const auto& item : *this)
|
||||
f(item);
|
||||
}
|
||||
};
|
||||
|
||||
typedef std::ostringstream ByteStream;
|
||||
|
||||
struct Request;
|
||||
@@ -203,6 +255,7 @@ struct Request {
|
||||
String websocket_scope = "";
|
||||
DValue* websocket_connection_state = 0;
|
||||
StringList websocket_scope_connection_ids;
|
||||
DValue websocket_connection_state_before;
|
||||
DValue websocket_dispatch_commands;
|
||||
bool websocket_dispatch_capture = false;
|
||||
u8 websocket_opcode = 0;
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@ String zip_error(String api, String detail)
|
||||
|
||||
void archive_check_size(String api, String label, u64 size, String config_key, u64 fallback)
|
||||
{
|
||||
u64 limit = config_u64(config_key, fallback);
|
||||
u64 limit = to_u64(context->server->config[config_key], fallback);
|
||||
if(limit > 0 && size > limit)
|
||||
throw std::runtime_error(zip_error(api, label + " exceeds configured limit"));
|
||||
}
|
||||
@@ -326,7 +326,7 @@ String gz_uncompress(String compressed)
|
||||
if(!out)
|
||||
throw std::runtime_error("gz_uncompress(): decompression failed");
|
||||
|
||||
u64 output_limit = config_u64("ARCHIVE_MAX_OUTPUT_BYTES", 64 * 1024 * 1024);
|
||||
u64 output_limit = to_u64(context->server->config["ARCHIVE_MAX_OUTPUT_BYTES"], 64 * 1024 * 1024);
|
||||
if(output_limit > 0 && out_len > output_limit)
|
||||
{
|
||||
mz_free(out);
|
||||
|
||||
+29
-14
@@ -20,10 +20,16 @@ pid_t proactive_compiler_pid = 0;
|
||||
// The central WS broker process: owns the WS port + every connection, forwards
|
||||
// renders to the worker pool over uce.sock, and applies ws_* commands flushed
|
||||
// back from workers. ws_broker_outbound holds in-flight async render
|
||||
// connections (fd -> bytes still to write; empty value means draining the reply).
|
||||
// connections and their enqueue timestamps.
|
||||
struct WsBrokerOutbound
|
||||
{
|
||||
String pending;
|
||||
f64 started_at;
|
||||
};
|
||||
|
||||
FastCGIServer ws_broker;
|
||||
pid_t ws_broker_pid = 0;
|
||||
std::map<int, String> ws_broker_outbound;
|
||||
std::map<int, WsBrokerOutbound> ws_broker_outbound;
|
||||
static sigjmp_buf request_fault_jmp;
|
||||
static volatile sig_atomic_t request_fault_active = 0;
|
||||
static volatile sig_atomic_t request_fault_signal = 0;
|
||||
@@ -768,11 +774,11 @@ int custom_server_bind_http(FastCGIServer& dispatcher, String bind)
|
||||
if(custom_server_is_numeric_port(bind))
|
||||
{
|
||||
u64 port = int_val(bind);
|
||||
u64 min_port = config_map_u64(server_state.config, "CUSTOM_SERVER_MIN_PORT", 1024);
|
||||
u64 max_port = config_map_u64(server_state.config, "CUSTOM_SERVER_MAX_PORT", 65535);
|
||||
u64 min_port = to_u64(server_state.config["CUSTOM_SERVER_MIN_PORT"], 1024);
|
||||
u64 max_port = to_u64(server_state.config["CUSTOM_SERVER_MAX_PORT"], 65535);
|
||||
if(port < min_port || port > max_port)
|
||||
throw std::runtime_error("server_start_http(): TCP port is outside configured custom server range");
|
||||
String bind_address = config_map_bool(server_state.config, "CUSTOM_SERVER_ALLOW_PUBLIC_BIND", false) ? "0.0.0.0" : "127.0.0.1";
|
||||
String bind_address = to_bool(server_state.config["CUSTOM_SERVER_ALLOW_PUBLIC_BIND"], false) ? "0.0.0.0" : "127.0.0.1";
|
||||
return(dispatcher.listen_http((unsigned)port, bind_address));
|
||||
}
|
||||
String socket_prefix = first(server_state.config["CUSTOM_SERVER_UNIX_SOCKET_PREFIX"], "/tmp/uce/custom-servers/");
|
||||
@@ -836,7 +842,7 @@ int custom_server_http_complete(FastCGIRequest& request)
|
||||
request.params["UCE_SERVE_HTTP_BIND"] = cfg["bind"];
|
||||
request.params["UCE_SERVE_HTTP_FUNCTION"] = cfg["function"];
|
||||
request.params["SCRIPT_FILENAME"] = cfg["file"];
|
||||
u64 timeout = config_map_u64(server_state.config, "CUSTOM_SERVER_HANDLER_TIMEOUT_SECONDS", 30);
|
||||
u64 timeout = to_u64(server_state.config["CUSTOM_SERVER_HANDLER_TIMEOUT_SECONDS"], 30);
|
||||
return(forward_request_to_worker(request, timeout > 0 ? (u32)timeout : 30));
|
||||
}
|
||||
|
||||
@@ -942,20 +948,28 @@ int ws_broker_ws_message(FastCGIRequest& request, const String& message, u8 opco
|
||||
int fd = ws_broker_connect_unix(first(server_state.config["FCGI_SOCKET_PATH"], "/run/uce.sock"));
|
||||
if(fd < 0)
|
||||
return(0);
|
||||
ws_broker_outbound[fd] = fcgi_build_request(params, "");
|
||||
ws_broker_outbound[fd] = {fcgi_build_request(params, ""), time_precise()};
|
||||
return(0);
|
||||
}
|
||||
|
||||
// Drive in-flight render forwards non-blocking: finish writing the request, then
|
||||
// drain/discard the reply (the unit's ws_* came back via the command socket).
|
||||
void ws_broker_drain_outbound()
|
||||
void ws_broker_drain_outbound(u64 timeout_seconds)
|
||||
{
|
||||
f64 now = time_precise();
|
||||
for(auto it = ws_broker_outbound.begin(); it != ws_broker_outbound.end(); )
|
||||
{
|
||||
int fd = it->first;
|
||||
String& pending = it->second;
|
||||
WsBrokerOutbound& outbound = it->second;
|
||||
String& pending = outbound.pending;
|
||||
bool done = false;
|
||||
if(!pending.empty())
|
||||
bool timed_out = timeout_seconds > 0 && now - outbound.started_at > (f64)timeout_seconds;
|
||||
if(timed_out)
|
||||
{
|
||||
fprintf(stderr, "ws broker dropping stale outbound forward fd=%i after %.1fs\n", fd, now - outbound.started_at);
|
||||
done = true;
|
||||
}
|
||||
if(!done && !pending.empty())
|
||||
{
|
||||
ssize_t n = ::send(fd, pending.data(), pending.size(), MSG_NOSIGNAL | MSG_DONTWAIT);
|
||||
if(n > 0)
|
||||
@@ -997,6 +1011,7 @@ void run_ws_broker()
|
||||
ws_broker.on_websocket_message = &ws_broker_ws_message;
|
||||
if(server_state.config["HTTP_PORT"] != "")
|
||||
ws_broker.listen_http(int_val(server_state.config["HTTP_PORT"]));
|
||||
u64 ws_broker_outbound_timeout_seconds = to_u64(server_state.config["WS_BROKER_OUTBOUND_TIMEOUT_SECONDS"], 30);
|
||||
if(server_state.config["WS_BROKER_SOCKET_PATH"] != "")
|
||||
{
|
||||
ws_broker.listen(server_state.config["WS_BROKER_SOCKET_PATH"]);
|
||||
@@ -1005,7 +1020,7 @@ void run_ws_broker()
|
||||
for(;;)
|
||||
{
|
||||
ws_broker.process(50);
|
||||
ws_broker_drain_outbound();
|
||||
ws_broker_drain_outbound(ws_broker_outbound_timeout_seconds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1094,7 +1109,7 @@ pid_t server_start_http(String key, String socket_fn_or_port, String call_uce_fi
|
||||
previous_config = custom_server_config_decode(file_get_contents(config_file));
|
||||
String new_config = custom_server_config_encode(key, "http", socket_fn_or_port, call_uce_filename, call_function);
|
||||
String task_key = custom_server_task_key(key);
|
||||
u64 max_servers = config_map_u64(server_state.config, "CUSTOM_SERVER_MAX_SERVERS", 16);
|
||||
u64 max_servers = to_u64(server_state.config["CUSTOM_SERVER_MAX_SERVERS"], 16);
|
||||
if(!file_exists(config_file) && max_servers > 0 && custom_server_registry_count() >= max_servers)
|
||||
throw std::runtime_error("server_start_http(): custom server quota exceeded");
|
||||
pid_t existing_pid = task_pid(task_key);
|
||||
@@ -1146,7 +1161,7 @@ void run_proactive_compiler()
|
||||
StringList compile_queue;
|
||||
background_context.server = &server_state;
|
||||
set_active_request(background_context);
|
||||
if(!config_map_bool(server_state.config, "PROACTIVE_COMPILE_ENABLED", true))
|
||||
if(!to_bool(server_state.config["PROACTIVE_COMPILE_ENABLED"], true))
|
||||
return;
|
||||
f64 check_interval = float_val(server_state.config["PROACTIVE_COMPILE_CHECK_INTERVAL"]);
|
||||
f64 failure_retry_interval = 0;
|
||||
@@ -1276,7 +1291,7 @@ bool proactive_compiler_alive()
|
||||
|
||||
void ensure_proactive_compiler()
|
||||
{
|
||||
if(!config_map_bool(server_state.config, "PROACTIVE_COMPILE_ENABLED", true))
|
||||
if(!to_bool(server_state.config["PROACTIVE_COMPILE_ENABLED"], true))
|
||||
return;
|
||||
if(float_val(server_state.config["PROACTIVE_COMPILE_CHECK_INTERVAL"]) <= 0)
|
||||
return;
|
||||
|
||||
+23
-20
@@ -1,7 +1,7 @@
|
||||
// W4 — FastCGI backend glue for the W3 wasm workspace runtime.
|
||||
//
|
||||
// Included into the native server TU (src/linux_fastcgi.cpp) after uce_lib.cpp,
|
||||
// so it shares String/DValue/config and the UCEB1 codec. Provides a
|
||||
// so it shares String/DValue/config and the UCEB2 codec. Provides a
|
||||
// Always-on wasm backend: every unit request is served through a per-request
|
||||
// wasm workspace. The legacy native dlopen execution path has been removed.
|
||||
|
||||
@@ -50,9 +50,9 @@ static String wasm_backend_ensure_started(Request* context)
|
||||
for(const char* key : { "BIN_DIRECTORY", "SESSION_PATH", "TMP_UPLOAD_PATH" })
|
||||
if(cfg[key] != "")
|
||||
wc.write_roots.push_back(cfg[key]);
|
||||
wc.memory_limit = (int64_t)config_u64("WASM_MEMORY_LIMIT_BYTES", 512ull * 1024 * 1024);
|
||||
wc.epoch_deadline_ticks = config_u64("WASM_EPOCH_DEADLINE_TICKS", 200);
|
||||
wc.verbose = config_bool("WASM_BACKEND_VERBOSE", false);
|
||||
wc.memory_limit = (int64_t)to_u64(cfg["WASM_MEMORY_LIMIT_BYTES"], 512ull * 1024 * 1024);
|
||||
wc.epoch_deadline_ticks = to_u64(cfg["WASM_EPOCH_DEADLINE_TICKS"], 200);
|
||||
wc.verbose = to_bool(cfg["WASM_BACKEND_VERBOSE"], false);
|
||||
|
||||
g_wasm_worker = new WasmWorker(wc);
|
||||
g_wasm_init_error = g_wasm_worker->init();
|
||||
@@ -65,7 +65,7 @@ static String wasm_backend_ensure_started(Request* context)
|
||||
|
||||
g_wasm_epoch_running.store(true);
|
||||
WasmWorker* worker = g_wasm_worker;
|
||||
u64 period_ms = config_u64("WASM_EPOCH_PERIOD_MS", 50);
|
||||
u64 period_ms = to_u64(cfg["WASM_EPOCH_PERIOD_MS"], 50);
|
||||
g_wasm_epoch_ticker = new std::thread([worker, period_ms] {
|
||||
while(g_wasm_epoch_running.load())
|
||||
{
|
||||
@@ -84,12 +84,13 @@ static bool wasm_artifact_exists(Request* context, const String& entry_unit)
|
||||
struct stat wasm_st;
|
||||
if(stat(wasm_path.c_str(), &wasm_st) != 0 || !S_ISREG(wasm_st.st_mode))
|
||||
return(false);
|
||||
// Require the artifact to be newer than the source. If it is stale,
|
||||
// dispatch compiles the unit on demand and then rechecks this predicate.
|
||||
struct stat src_st;
|
||||
if(stat(entry_unit.c_str(), &src_st) != 0 || !S_ISREG(src_st.st_mode))
|
||||
// Require the artifact to satisfy the full compiler freshness check. Source
|
||||
// mtime alone misses runtime/unit ABI changes, setup-template changes, and
|
||||
// metadata mismatches, which can leave stale wasm with old imports.
|
||||
bool source_missing = false;
|
||||
if(compiler_unit_needs_recompile(context, entry_unit, &source_missing))
|
||||
return(false);
|
||||
if(wasm_st.st_mtime < src_st.st_mtime)
|
||||
if(source_missing)
|
||||
return(false);
|
||||
return(true);
|
||||
}
|
||||
@@ -151,23 +152,25 @@ String wasm_backend_serve(Request& request, const String& entry_unit, const Stri
|
||||
if(!response.ok)
|
||||
return(response.error == "" ? String("wasm workspace failed") : response.error);
|
||||
|
||||
// Any handler may have called ws_send/ws_close (not just WS handlers). If it
|
||||
// left dispatch commands, flush the batch to the central WS broker, which owns
|
||||
// all connections and resolves each command's target (id/scope/broadcast)
|
||||
// against the full registry. This is the only path WS data takes out — the
|
||||
// workspace owns no connections.
|
||||
if(DValue* cmds = response.meta.key("ws_commands"))
|
||||
// Any handler may have called ws_send/ws_close (not just WS handlers). Flush the
|
||||
// websocket batch whenever either command frames or an updated per-connection
|
||||
// state are present. This is the only path WS data takes out; the workspace
|
||||
// owns no connections.
|
||||
DValue* cmds = response.meta.key("ws_commands");
|
||||
DValue* cstate = response.meta.key("ws_connection_state");
|
||||
if(cmds || cstate)
|
||||
{
|
||||
DValue batch;
|
||||
batch["commands"] = *cmds;
|
||||
if(DValue* cstate = response.meta.key("ws_connection_state"))
|
||||
if(cmds)
|
||||
batch["commands"] = *cmds;
|
||||
if(cstate)
|
||||
{
|
||||
batch["connection_id"] = request.resources.websocket_connection_id;
|
||||
batch["connection_state"] = *cstate;
|
||||
}
|
||||
StringMap dispatch_params;
|
||||
dispatch_params["UCE_WS_DISPATCH"] = "1";
|
||||
String broker_socket = first(request.server ? request.server->config["WS_BROKER_SOCKET_PATH"] : String(),
|
||||
String broker_socket = first(request.server->config["WS_BROKER_SOCKET_PATH"],
|
||||
"/run/uce/ws-broker.sock");
|
||||
fcgi_forward_request(broker_socket, dispatch_params, ucb_encode(batch), 5);
|
||||
}
|
||||
@@ -182,7 +185,7 @@ String wasm_backend_serve(Request& request, const String& entry_unit, const Stri
|
||||
|
||||
// Diagnostic timing headers are opt-in: they leak workspace internals and
|
||||
// belong to the W5 benchmark harness, not public responses.
|
||||
if(config_bool("WASM_BACKEND_VERBOSE", false))
|
||||
if(to_bool(request.server->config["WASM_BACKEND_VERBOSE"], false))
|
||||
{
|
||||
request.header["X-UCE-Backend"] = "wasm";
|
||||
request.header["X-UCE-Wasm-Workspace-Birth-Us"] = std::to_string(response.workspace_birth_us);
|
||||
|
||||
+19
-8
@@ -12,7 +12,7 @@
|
||||
|
||||
// ---- W3 connector membranes -----------------------------------------------
|
||||
// sqlite/mysql run host-side (the host links the native connectors and owns the
|
||||
// connections in per-workspace handle tables). UCEB1-marshalled hostcalls carry
|
||||
// connections in per-workspace handle tables). UCEB2-marshalled hostcalls carry
|
||||
// operation requests/responses; `connection` holds the host handle (>0).
|
||||
|
||||
static const char* WASM_DB_UNAVAILABLE =
|
||||
@@ -303,7 +303,7 @@ DValue MySQL::query(String q, StringMap params) { return(query(parse_query_param
|
||||
DValue MySQL::get_pending_result() { return(DValue()); }
|
||||
|
||||
// sqlite runs host-side (the host links libsqlite and owns the connections in
|
||||
// a per-workspace handle table). One UCEB1-marshalled hostcall carries
|
||||
// a per-workspace handle table). One UCEB2-marshalled hostcall carries
|
||||
// {op,handle,path,query,params} in and {handle,result,insert_id,affected,
|
||||
// error_code,statement_info} out. `connection` holds the host handle (>0).
|
||||
extern "C" size_t uce_host_sqlite(const char* in, size_t in_len, char* out, size_t cap);
|
||||
@@ -766,7 +766,7 @@ void uce_wasm_core_reset_request()
|
||||
wasm_component_slots.clear();
|
||||
}
|
||||
|
||||
// Host pushes the UCEB1-encoded request context into a guest buffer
|
||||
// Host pushes the UCEB2-encoded request context into a guest buffer
|
||||
// (uce_alloc) and applies it here; mirrors the native param population.
|
||||
int uce_wasm_apply_context(const char* buf, size_t len)
|
||||
{
|
||||
@@ -800,6 +800,8 @@ int uce_wasm_apply_context(const char* buf, size_t len)
|
||||
// websocket event context: ws_send()/ws_close() capture into the dispatch
|
||||
// list (the workspace owns no connections), which collect() carries back to
|
||||
// the broker. Reset per invocation.
|
||||
wasm_request.connection = DValue();
|
||||
wasm_request.resources.websocket_connection_state_before = DValue();
|
||||
wasm_request.resources.websocket_dispatch_commands = DValue();
|
||||
wasm_request.resources.websocket_dispatch_capture = false;
|
||||
DValue* ws = decoded.key("ws");
|
||||
@@ -817,6 +819,7 @@ int uce_wasm_apply_context(const char* buf, size_t len)
|
||||
wasm_request.resources.websocket_dispatch_capture = true;
|
||||
if(DValue* cstate = ws->key("connection_state"))
|
||||
wasm_request.connection = *cstate;
|
||||
wasm_request.resources.websocket_connection_state_before = wasm_request.connection;
|
||||
}
|
||||
return(0);
|
||||
}
|
||||
@@ -829,7 +832,7 @@ Request* uce_wasm_request()
|
||||
}
|
||||
|
||||
// After render: response metadata (status line, headers, cookies, session)
|
||||
// goes back to the host as UCEB1.
|
||||
// goes back to the host as UCEB2.
|
||||
void uce_wasm_finish_response_meta()
|
||||
{
|
||||
DValue meta;
|
||||
@@ -844,14 +847,22 @@ void uce_wasm_finish_response_meta()
|
||||
}
|
||||
for(auto& entry : wasm_request.session)
|
||||
meta["session"][entry.first] = entry.second;
|
||||
bool ws_has_commands = !wasm_request.resources.websocket_dispatch_commands._map.empty();
|
||||
bool ws_state_changed = false;
|
||||
if(wasm_request.resources.websocket_dispatch_capture)
|
||||
{
|
||||
String prior_state = ucb_encode(wasm_request.resources.websocket_connection_state_before);
|
||||
String current_state = ucb_encode(wasm_request.connection);
|
||||
ws_state_changed = (prior_state != current_state);
|
||||
}
|
||||
// Any unit code (not just WS handlers) may call ws_send/ws_close; whenever the
|
||||
// dispatch list is non-empty, carry it back so the worker can flush it to the
|
||||
// broker. ws_connection_state rides along for stateful WS handlers.
|
||||
if(!wasm_request.resources.websocket_dispatch_commands._map.empty())
|
||||
{
|
||||
// broker. If only connection state changed, flush a command-less state-only
|
||||
// batch so the broker can persist the connection mutation.
|
||||
if(ws_has_commands)
|
||||
meta["ws_commands"] = wasm_request.resources.websocket_dispatch_commands;
|
||||
if(ws_state_changed)
|
||||
meta["ws_connection_state"] = wasm_request.connection;
|
||||
}
|
||||
wasm_response_meta = ucb_encode(meta);
|
||||
}
|
||||
|
||||
|
||||
@@ -280,7 +280,7 @@ int main(int argc, char** argv)
|
||||
int32_t encoded_ptr = call_i32(core, "uce_alloc", { encoded_len });
|
||||
CHECK(call_i32(core, "uce_dv_encode", { root, encoded_ptr, encoded_len }) == encoded_len, "encode length mismatch");
|
||||
std::string encoded = read_bytes(memory, encoded_ptr, encoded_len);
|
||||
CHECK(encoded.rfind("UCEB\x01", 0) == 0, "UCEB1 header missing");
|
||||
CHECK(encoded.size() >= 5 && encoded.compare(0, 4, "UCEB") == 0 && (unsigned char)encoded[4] == 2, "UCEB2 header missing");
|
||||
int32_t decoded = call_i32(core, "uce_dv_decode", { encoded_ptr, encoded_len });
|
||||
CHECK(decoded != 0, "uce_dv_decode failed");
|
||||
CHECK(call_i32(core, "uce_dv_count", { decoded }) == 1, "decoded root count mismatch");
|
||||
@@ -290,8 +290,8 @@ int main(int argc, char** argv)
|
||||
std::string bad = "bad";
|
||||
int32_t bad_ptr = call_i32(core, "uce_alloc", { (int32_t)bad.size() });
|
||||
write_bytes(memory, bad_ptr, bad);
|
||||
CHECK(call_i32(core, "uce_dv_decode", { bad_ptr, (int32_t)bad.size() }) == 0, "bad UCEB1 decode unexpectedly succeeded");
|
||||
CHECK(read_cstr(memory, call_i32(core, "uce_dv_last_error")) != "", "bad UCEB1 decode did not set error");
|
||||
CHECK(call_i32(core, "uce_dv_decode", { bad_ptr, (int32_t)bad.size() }) == 0, "bad UCEB2 decode unexpectedly succeeded");
|
||||
CHECK(read_cstr(memory, call_i32(core, "uce_dv_last_error")) != "", "bad UCEB2 decode did not set error");
|
||||
|
||||
std::string out = "W1 output";
|
||||
int32_t out_ptr = call_i32(core, "uce_alloc", { (int32_t)out.size() });
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// W3 CLI driver — the exit gate for WASM-PROPOSAL §9.1 W3.
|
||||
//
|
||||
// Serves requests through the production workspace runtime
|
||||
// (src/wasm/worker.cpp): UCEB1 context in → core + lazily loaded
|
||||
// (src/wasm/worker.cpp): UCEB2 context in → core + lazily loaded
|
||||
// generated units → body/response-meta out. Each --repeat gets a fresh
|
||||
// workspace, proving birth/drop. An epoch ticker thread enforces the CPU
|
||||
// budget; the store limiter enforces memory; traps come back as collapsed
|
||||
|
||||
+57
-2
@@ -1012,6 +1012,56 @@ private:
|
||||
return(stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode));
|
||||
}
|
||||
|
||||
// Keep cwd host behavior local to this process but guard it with the same
|
||||
// write-root policy we use for file writes (plus a single parity fallback).
|
||||
String resolve_guest_cwd_set(const String& raw)
|
||||
{
|
||||
if(raw == "" || raw.find('\0') != String::npos)
|
||||
return("");
|
||||
|
||||
String raw_target = raw;
|
||||
if(raw.rfind("/", 0) != 0)
|
||||
{
|
||||
String cwd = ::cwd_get();
|
||||
if(cwd == "")
|
||||
return("");
|
||||
raw_target = cwd + "/" + raw;
|
||||
}
|
||||
|
||||
char resolved[PATH_MAX];
|
||||
if(!realpath(raw_target.c_str(), resolved))
|
||||
return("");
|
||||
String resolved_target(resolved);
|
||||
if(!dir_exists_host(resolved_target))
|
||||
return("");
|
||||
|
||||
// Policy: allow only roots we already expose for writable filesystem access.
|
||||
std::vector<String> roots;
|
||||
roots.push_back(worker.cfg.site_root);
|
||||
for(auto& root : worker.cfg.write_roots)
|
||||
roots.push_back(root);
|
||||
for(auto& root : roots)
|
||||
{
|
||||
if(root == "")
|
||||
continue;
|
||||
char root_real[PATH_MAX];
|
||||
if(!realpath(root.c_str(), root_real))
|
||||
continue;
|
||||
String canonical_root(root_real);
|
||||
if(resolved_target == canonical_root)
|
||||
return(resolved_target);
|
||||
if(canonical_root != "/" && resolved_target.rfind(canonical_root + "/", 0) == 0)
|
||||
return(resolved_target);
|
||||
}
|
||||
|
||||
// Parity/fallback: allow returning to the process start directory so
|
||||
// legacy behavior is not silently broken for existing native/cached flows.
|
||||
String start_directory = ::process_start_directory();
|
||||
if(start_directory != "" && resolved_target == start_directory)
|
||||
return(resolved_target);
|
||||
return("");
|
||||
}
|
||||
|
||||
String resolve_source_path(const String& file_name, const String& current_unit)
|
||||
{
|
||||
std::vector<String> bases;
|
||||
@@ -1378,7 +1428,8 @@ private:
|
||||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||||
String path;
|
||||
self->hostcall_read(args[0].i32(), args[1].i32(), path);
|
||||
results[0] = Val(::chdir(path.c_str()) == 0 ? (int32_t)1 : (int32_t)0);
|
||||
String resolved = self->resolve_guest_cwd_set(path);
|
||||
results[0] = Val(::chdir(resolved.c_str()) == 0 ? (int32_t)1 : (int32_t)0);
|
||||
return(std::monostate());
|
||||
}));
|
||||
if(mod == "env" && name == "uce_host_process_start_directory")
|
||||
@@ -1991,6 +2042,10 @@ private:
|
||||
f64 interval = args[3].f64();
|
||||
u64 timeout = (u64)args[4].i64();
|
||||
bool repeat = args[5].i32() != 0;
|
||||
// task()/task_repeat() fork and invoke this lambda only in the child
|
||||
// before the hostcall stack unwinds, so `self` points to the child's
|
||||
// copy of this per-request workspace. The parent request can return and
|
||||
// destroy its workspace without invalidating the child copy.
|
||||
auto run_callback = [self, callback_id]() {
|
||||
String error = self->run_task_callback(callback_id);
|
||||
if(error != "")
|
||||
@@ -2049,7 +2104,7 @@ private:
|
||||
}));
|
||||
if(mod == "env" && name == "uce_host_regex")
|
||||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||||
// {op,pattern,subject,flags,replacement} in (UCEB1) → result out.
|
||||
// {op,pattern,subject,flags,replacement} in (UCEB2) → result out.
|
||||
// PCRE2 lives host-side; this runs the native regex_*.
|
||||
String encoded;
|
||||
self->hostcall_read(args[0].i32(), args[1].i32(), encoded);
|
||||
|
||||
Reference in New Issue
Block a user