mirror of
https://github.com/torlando-tech/pyxis.git
synced 2026-08-28 13:34:17 +00:00
718 lines
32 KiB
C++
718 lines
32 KiB
C++
#include <microStore/Adapters/UniversalFileSystem.h>
|
|
#include <microStore/FileSystem.h>
|
|
#include <UDPInterface.h>
|
|
#include <microReticulum.h>
|
|
#include "TCPClientInterface.h"
|
|
|
|
#include "NomadNetActionMailbox.h"
|
|
#include "NomadNetDocument.h"
|
|
#include "NomadNetCompactPage.h"
|
|
#include "NomadNetForm.h"
|
|
#include "NomadNetHistory.h"
|
|
#include "NomadNetLibrary.h"
|
|
#include "NomadNetMailbox.h"
|
|
#include "NomadNetOwner.h"
|
|
#include "NomadNetPartialController.h"
|
|
#include "NomadNetPartialScheduler.h"
|
|
#include "NomadNetProtocol.h"
|
|
#include "NomadNetUrl.h"
|
|
#include "BuildManifest.h"
|
|
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace NN = UI::LXMF::NomadNet;
|
|
|
|
static RNS::Reticulum reticulum({RNS::Type::NONE});
|
|
static RNS::Interface network_interface({RNS::Type::NONE});
|
|
static TCPClientInterface* tcp_interface = nullptr;
|
|
static RNS::Destination destination({RNS::Type::NONE});
|
|
static RNS::Link active_link({RNS::Type::NONE});
|
|
static RNS::RequestReceipt receipt({RNS::Type::NONE});
|
|
static RNS::Identity local_identity({RNS::Type::NONE});
|
|
static NN::AsyncMailbox mailbox;
|
|
static NN::ActionMailbox actions;
|
|
static std::string scenario;
|
|
static std::string destination_hex;
|
|
static std::string expected_destination_hex;
|
|
static bool announce_seen = false;
|
|
static bool link_established = false;
|
|
static bool request_started = false;
|
|
static double request_started_at = 0.0;
|
|
static bool application_deadline_fired = false;
|
|
static bool completed = false;
|
|
static bool passed = false;
|
|
static bool progress_seen = false;
|
|
static int progress_callbacks = 0;
|
|
static bool cancellation_started = false;
|
|
static double cancellation_at = 0.0;
|
|
static int stale_callback_rejections = 0;
|
|
static bool resource_started = false;
|
|
static bool resource_progress = false;
|
|
static bool receipt_failed = false;
|
|
static bool pending_empty = false;
|
|
static bool link_closed = false;
|
|
static int link_callbacks = 0;
|
|
static int reuse_requests = 0;
|
|
static NN::PageHistory owner_history;
|
|
static NN::PageHistory::PendingOpen owner_pending_history;
|
|
static int owner_requests = 0;
|
|
static bool owner_submit = false;
|
|
static bool owner_history_bytes = false;
|
|
static bool owner_retained_link = false;
|
|
static bool owner_back_restored = false;
|
|
static bool owner_reload_reused = false;
|
|
static NN::ExternalVector<uint8_t> owner_first_request;
|
|
static NN::OwnerController owner;
|
|
static NN::PartialScheduler partial_scheduler;
|
|
static NN::PartialController partial_controller;
|
|
static NN::PartialRequest partial_request;
|
|
static NN::CompactPage partial_page;
|
|
static bool partial_live = false;
|
|
|
|
class ScreenSubmissionSource final : public NN::OwnerSubmissionSource {
|
|
public:
|
|
explicit ScreenSubmissionSource(const char* value) : _value(value) {}
|
|
bool prepare_submission(uint16_t, uint32_t, std::string& target,
|
|
NN::ExternalVector<uint8_t>& output,
|
|
NN::FormEncodeResult& result) override {
|
|
NN::DocumentParser parser;
|
|
const auto document = parser.parse(
|
|
"`<name`Initial> `<!|password`Initial> "
|
|
"`<?|color|red|*`Red> `<?|color|blue|*`Blue> "
|
|
"`[:/page/form.mu`name|password|color|fixed=yes`Submit]");
|
|
NN::CompactPage page;
|
|
NN::FormState state;
|
|
if (!page.assign(document) || !state.assign(page) ||
|
|
!state.set_value(0, _value) || !state.set_value(1, "example-pass")) {
|
|
result = NN::FormEncodeResult::ALLOCATION_FAILED;
|
|
return false;
|
|
}
|
|
result = state.encode("name|password|color|fixed=yes", output);
|
|
target = destination_hex + ":/page/form.mu";
|
|
return result == NN::FormEncodeResult::OK;
|
|
}
|
|
private:
|
|
const char* _value;
|
|
};
|
|
|
|
static bool prepare_form_request(NN::ExternalVector<uint8_t>& output) {
|
|
NN::DocumentParser parser;
|
|
const auto document = parser.parse(
|
|
"`<name`Initial> `<!|password`Initial> "
|
|
"`<?|color|red|*`Red> `<?|color|blue|*`Blue>");
|
|
NN::CompactPage page;
|
|
NN::FormState state;
|
|
if (!page.assign(document) || !state.assign(page) ||
|
|
!state.set_value(0, "Example User") ||
|
|
!state.set_value(1, "example-pass")) return false;
|
|
return state.encode("name|password|color|fixed=yes", output) ==
|
|
NN::FormEncodeResult::OK;
|
|
}
|
|
|
|
static std::vector<uint8_t> bytes_vector(const RNS::Bytes& bytes) {
|
|
if (bytes.size() == 0) return {};
|
|
return std::vector<uint8_t>(bytes.data(), bytes.data() + bytes.size());
|
|
}
|
|
|
|
static void fail(const char* message) {
|
|
std::printf("FAIL %s\n", message);
|
|
completed = true;
|
|
passed = false;
|
|
}
|
|
|
|
static bool validate_page(const RNS::Bytes& response, bool expect_large) {
|
|
NN::ResponseBuffer normalized;
|
|
if (!NN::normalize_response(response.data(), response.size(), normalized)) {
|
|
fail("response normalization");
|
|
return false;
|
|
}
|
|
const std::string source(reinterpret_cast<const char*>(normalized.bytes().data()), normalized.size());
|
|
NN::Document document = NN::DocumentParser().parse(source);
|
|
if (document.malformed || document.truncated || document.blocks.empty()) {
|
|
fail("Micron parse");
|
|
return false;
|
|
}
|
|
const bool form_scenario = scenario == "form-anonymous" || scenario == "form-identified" ||
|
|
scenario == "owner-form-history";
|
|
const char* marker = form_scenario ? "Form response" :
|
|
(expect_large ? "Resource-backed page" : "Immediate page");
|
|
std::string lan_heading;
|
|
if (scenario == "lan") {
|
|
for (const auto& block : document.blocks) {
|
|
if (block.type != NN::BlockType::HEADING) continue;
|
|
for (const auto& run : block.runs) lan_heading += run.text;
|
|
if (!lan_heading.empty()) break;
|
|
}
|
|
std::printf("LAN PAGE bytes=%zu blocks=%zu links=%zu heading=%s\n",
|
|
normalized.size(), document.blocks.size(), document.links.size(),
|
|
lan_heading.c_str());
|
|
if (source.empty()) {
|
|
fail("LAN page empty");
|
|
return false;
|
|
}
|
|
marker = lan_heading.empty() ? "NomadNet LAN page" : lan_heading.c_str();
|
|
} else if (source.find(marker) == std::string::npos) {
|
|
fail("page content marker");
|
|
return false;
|
|
}
|
|
|
|
const std::string path = form_scenario ? "/page/form.mu" :
|
|
scenario == "lan" ? "/page/index.mu" :
|
|
scenario == "near-limit" ? "/page/near-limit.mu" :
|
|
(expect_large ? "/page/resource.mu" : "/page/immediate.mu");
|
|
const std::string url = destination_hex + ":" + path;
|
|
NN::Url parsed;
|
|
std::string error;
|
|
if (!NN::Url::parse(url, parsed, error) || parsed.destination_hex != destination_hex || parsed.path != path) {
|
|
fail("absolute URL parse");
|
|
return false;
|
|
}
|
|
NN::Url relative;
|
|
if (!NN::Url::parse(":/page/next.mu", relative, error, destination_hex) ||
|
|
relative.destination_hex != destination_hex || relative.path != "/page/next.mu") {
|
|
fail("relative URL parse");
|
|
return false;
|
|
}
|
|
|
|
NN::Library library;
|
|
if (!library.hear_node(destination_hex, "x86 NomadNet peer", 100, 1) ||
|
|
!library.record_page(url, marker, 101) ||
|
|
!library.set_page_saved(url, true)) {
|
|
fail("library record/save");
|
|
return false;
|
|
}
|
|
const auto encoded = library.encode();
|
|
NN::Library restored;
|
|
if (encoded.empty() || !restored.decode(encoded.data(), encoded.size()) ||
|
|
!restored.page_saved(url) || restored.nodes().size() != 1 || restored.pages().size() != 1) {
|
|
fail("library persistence reload");
|
|
return false;
|
|
}
|
|
|
|
// Exercise the Save -> Back ordering used by the owner loop. Save remains
|
|
// durable work, but the terminal action is guaranteed to follow it.
|
|
if (scenario == "owner-form-history") return true;
|
|
if (!actions.publish(NN::UserActionKind::SAVE, url) ||
|
|
!actions.publish(NN::UserActionKind::BACK, "")) {
|
|
fail("Save/Back publication");
|
|
return false;
|
|
}
|
|
NN::UserAction action;
|
|
if (!actions.pop(action) || action.kind != NN::UserActionKind::SAVE ||
|
|
!actions.pop(action) || action.kind != NN::UserActionKind::BACK) {
|
|
fail("Save/Back ordering");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void on_response(const RNS::RequestReceipt& callback_receipt) {
|
|
const RNS::Bytes token = callback_receipt.get_request_id();
|
|
const RNS::Bytes response = callback_receipt.get_response();
|
|
if (!mailbox.publish_response(bytes_vector(token), response.data(), response.size(),
|
|
callback_receipt.response_transfer_size())) {
|
|
++stale_callback_rejections;
|
|
}
|
|
}
|
|
|
|
static void on_failed(const RNS::RequestReceipt& callback_receipt) {
|
|
const auto token = bytes_vector(callback_receipt.get_request_id());
|
|
const bool published = callback_receipt.response_size() > NN::AsyncMailbox::MAX_WIRE_BYTES
|
|
? mailbox.publish_oversized(token, callback_receipt.response_size())
|
|
: mailbox.publish_failed(token);
|
|
if (!published) {
|
|
++stale_callback_rejections;
|
|
}
|
|
}
|
|
|
|
static void on_progress(const RNS::RequestReceipt& callback_receipt) {
|
|
++progress_callbacks;
|
|
std::printf("CALLBACK progress status=%d transfer=%zu response=%llu progress=%.3f\n",
|
|
static_cast<int>(callback_receipt.get_status()),
|
|
callback_receipt.response_transfer_size(),
|
|
static_cast<unsigned long long>(callback_receipt.response_size()),
|
|
callback_receipt.get_progress());
|
|
if (!mailbox.publish_progress(bytes_vector(callback_receipt.get_request_id()),
|
|
callback_receipt.response_transfer_size())) {
|
|
++stale_callback_rejections;
|
|
}
|
|
}
|
|
|
|
static void on_resource_progress(const RNS::Resource& resource) {
|
|
const float progress = resource.get_progress();
|
|
if (progress <= 0.0f || progress >= 1.0f) return;
|
|
resource_progress = true;
|
|
std::printf("RESOURCE progress=%.3f transfer=%zu data=%zu\n",
|
|
progress, resource.get_transfer_size(), resource.get_data_size());
|
|
if (scenario == "cancel" && !cancellation_started) {
|
|
actions.publish(NN::UserActionKind::BACK, "");
|
|
}
|
|
}
|
|
|
|
static void on_resource_started(const RNS::Resource& resource) {
|
|
resource_started = true;
|
|
std::printf("RESOURCE started transfer=%zu data=%zu\n",
|
|
resource.get_transfer_size(), resource.get_data_size());
|
|
const_cast<RNS::Resource&>(resource).set_progress_callback(on_resource_progress);
|
|
}
|
|
|
|
static void on_link_closed(RNS::Link& closed_link) {
|
|
if (!mailbox.publish_link(bytes_vector(closed_link.link_id()), false)) {
|
|
++stale_callback_rejections;
|
|
}
|
|
}
|
|
|
|
static bool owner_request_data(const NN::ExternalVector<uint8_t>& request_data,
|
|
RNS::Link* established = nullptr) {
|
|
RNS::Link& request_link = established ? *established : active_link;
|
|
receipt = request_link.request(RNS::Bytes("/page/form.mu"),
|
|
RNS::Bytes(request_data.data(), request_data.size()), on_response, on_failed,
|
|
on_progress, 8.0, NN::AsyncMailbox::MAX_WIRE_BYTES);
|
|
if (!receipt) return false;
|
|
mailbox.expect_request(bytes_vector(receipt.get_request_id()));
|
|
request_started = true;
|
|
request_started_at = RNS::Utilities::OS::time();
|
|
++owner_requests;
|
|
return true;
|
|
}
|
|
|
|
static bool owner_request_current(RNS::Link* established = nullptr) {
|
|
return owner_request_data(owner_history.current_request_data(), established);
|
|
}
|
|
|
|
static bool service_submit_through_owner(const char* name, RNS::Link* established = nullptr) {
|
|
NN::UserAction action;
|
|
if (!actions.pop(action) || action.kind != NN::UserActionKind::SUBMIT) return false;
|
|
ScreenSubmissionSource screen(name);
|
|
auto command = owner.service(action, owner_history, screen, 0);
|
|
if (command.result != NN::OwnerResult::REQUEST) return false;
|
|
owner_submit = true;
|
|
if (owner_requests == 0) owner_first_request.assign(
|
|
command.request_data.begin(), command.request_data.end());
|
|
owner_pending_history = std::move(command.pending_history);
|
|
return owner_pending_history.ready() && owner_request_data(command.request_data, established);
|
|
}
|
|
|
|
static void on_link_established(RNS::Link& established_link) {
|
|
// Link construction invokes this callback before the assigning expression
|
|
// completes; take the established production handle before owner requests.
|
|
active_link = established_link;
|
|
|
|
link_established = true;
|
|
++link_callbacks;
|
|
mailbox.begin(bytes_vector(established_link.link_id()));
|
|
established_link.set_resource_started_callback(on_resource_started);
|
|
std::string path;
|
|
double timeout = 8.0;
|
|
if (scenario == "partial") {
|
|
const std::string source = "Before\n`{" + destination_hex +
|
|
":/page/partial.mu}\nAfter";
|
|
const NN::Document document = NN::DocumentParser().parse(source);
|
|
const auto nil = NN::no_form_request_data();
|
|
if (document.malformed || document.partials.size() != 1 ||
|
|
!partial_page.assign(document)) {
|
|
fail("partial base parse");
|
|
return;
|
|
}
|
|
partial_scheduler.configure(partial_page, 1U, 0U);
|
|
partial_controller.reset_page(document.source_bytes);
|
|
if (!partial_scheduler.poll(0U, true, true, partial_request) ||
|
|
!partial_controller.prepare(partial_request, partial_page,
|
|
nil.data(), nil.size())) {
|
|
fail("partial lease preparation");
|
|
return;
|
|
}
|
|
path = "/page/partial.mu";
|
|
} else if (scenario == "immediate") path = "/page/immediate.mu";
|
|
else if (scenario == "reuse") path = "/page/reuse-first.mu";
|
|
else if (scenario == "lan") path = "/page/index.mu";
|
|
else if (scenario == "resource") path = "/page/resource.mu";
|
|
else if (scenario == "near-limit") path = "/page/near-limit.mu";
|
|
else if (scenario == "oversized") path = "/page/oversized.mu";
|
|
else if (scenario == "cancel") path = "/page/cancel.mu";
|
|
else if (scenario == "form-anonymous" || scenario == "form-identified" ||
|
|
scenario == "owner-form-history")
|
|
path = "/page/form.mu";
|
|
else {
|
|
path = "/page/missing.mu";
|
|
timeout = 1.5;
|
|
}
|
|
NN::ExternalVector<uint8_t> request_data;
|
|
if (scenario == "owner-form-history") {
|
|
if (!actions.publish_submit(0, 1) ||
|
|
!service_submit_through_owner("Example User", &established_link)) {
|
|
fail("production-owner initial Submit");
|
|
}
|
|
return;
|
|
} else if (scenario == "form-anonymous" || scenario == "form-identified") {
|
|
if (!prepare_form_request(request_data)) {
|
|
fail("form request encoding");
|
|
return;
|
|
}
|
|
if (scenario == "form-identified") established_link.identify(local_identity);
|
|
} else {
|
|
const auto nil = NN::no_form_request_data();
|
|
request_data.assign(nil.begin(), nil.end());
|
|
}
|
|
receipt = established_link.request(RNS::Bytes(path),
|
|
RNS::Bytes(request_data.data(), request_data.size()),
|
|
on_response, on_failed, on_progress, timeout,
|
|
scenario == "partial"
|
|
? NN::PartialController::MAX_RESPONSE_WIRE_BYTES
|
|
: NN::AsyncMailbox::MAX_WIRE_BYTES);
|
|
NN::clear_encoded_form(request_data);
|
|
if (!receipt) {
|
|
fail("request creation");
|
|
return;
|
|
}
|
|
mailbox.expect_request(bytes_vector(receipt.get_request_id()));
|
|
request_started = true;
|
|
if (scenario == "reuse") ++reuse_requests;
|
|
request_started_at = RNS::Utilities::OS::time();
|
|
}
|
|
|
|
class NomadAnnounceHandler : public RNS::AnnounceHandler {
|
|
public:
|
|
NomadAnnounceHandler() : RNS::AnnounceHandler("nomadnetwork.node") {}
|
|
|
|
void received_announce(const RNS::Bytes& destination_hash,
|
|
const RNS::Identity& identity,
|
|
const RNS::Bytes&) override {
|
|
if (announce_seen) return;
|
|
if (!expected_destination_hex.empty() && destination_hash.toHex() != expected_destination_hex) return;
|
|
announce_seen = true;
|
|
destination_hex = destination_hash.toHex();
|
|
if (!RNS::Transport::has_path(destination_hash)) {
|
|
fail("announce did not install local path");
|
|
return;
|
|
}
|
|
destination = RNS::Destination(identity, RNS::Type::Destination::OUT,
|
|
RNS::Type::Destination::SINGLE,
|
|
"nomadnetwork", "node");
|
|
mailbox.prepare(0, scenario == "partial"
|
|
? NN::PartialController::MAX_RESPONSE_WIRE_BYTES
|
|
: NN::AsyncMailbox::MAX_WIRE_BYTES);
|
|
active_link = RNS::Link(destination, on_link_established, on_link_closed);
|
|
// Deliberately no link.identify(): ordinary Pyxis page retrieval is anonymous.
|
|
}
|
|
};
|
|
|
|
static RNS::HAnnounceHandler announce_handler(new NomadAnnounceHandler());
|
|
|
|
static void consume_event() {
|
|
NN::AsyncMailbox::Event event;
|
|
if (!mailbox.take(event)) return;
|
|
switch (event.kind) {
|
|
case NN::AsyncMailbox::Kind::PROGRESS:
|
|
progress_seen = true;
|
|
break;
|
|
case NN::AsyncMailbox::Kind::RESPONSE: {
|
|
if (scenario == "cancel") {
|
|
fail("response arrived after Back cancellation");
|
|
return;
|
|
}
|
|
const RNS::Bytes response(event.data.data(), event.data.size());
|
|
if (scenario == "partial") {
|
|
NN::ResponseBuffer normalized;
|
|
NN::CompactPage candidate;
|
|
if (!partial_controller.active() ||
|
|
!partial_controller.matches(partial_request) ||
|
|
!NN::normalize_response(response.data(), response.size(), normalized) ||
|
|
normalized.size() > NN::PartialController::MAX_RESPONSE_BYTES) {
|
|
fail("partial response ownership");
|
|
return;
|
|
}
|
|
const std::string source(
|
|
reinterpret_cast<const char*>(normalized.bytes().data()),
|
|
normalized.size());
|
|
const NN::Document fragment = NN::DocumentParser().parse(source);
|
|
if (fragment.malformed || fragment.truncated ||
|
|
!partial_controller.can_accept_fragment(
|
|
partial_request.partial_index, normalized.size()) ||
|
|
candidate.assign_replacing_partial(
|
|
partial_page, partial_request.partial_index, fragment,
|
|
NN::PartialController::MAX_EXPANDED_SOURCE_BYTES) !=
|
|
NN::PartialReplaceResult::APPLIED ||
|
|
!partial_controller.commit_fragment(
|
|
partial_request.partial_index, normalized.size())) {
|
|
fail("partial transactional replacement");
|
|
return;
|
|
}
|
|
partial_page = std::move(candidate);
|
|
std::string rendered;
|
|
for (const auto& run : partial_page.runs()) {
|
|
const auto text = partial_page.text(run);
|
|
rendered.append(text.data(), text.size());
|
|
}
|
|
partial_live = rendered.find("Peer-refreshed fragment") != std::string::npos &&
|
|
partial_scheduler.complete(partial_request, true, 1U);
|
|
partial_controller.abandon_request();
|
|
passed = partial_live;
|
|
completed = true;
|
|
if (!passed) fail("partial rendered content");
|
|
break;
|
|
}
|
|
const bool expected_resource = scenario == "resource" || scenario == "near-limit" ||
|
|
(scenario == "reuse" && reuse_requests == 2);
|
|
if (expected_resource && progress_callbacks == 0) {
|
|
fail("Resource response had no progress callback");
|
|
return;
|
|
}
|
|
passed = validate_page(response, expected_resource);
|
|
if (!passed) return;
|
|
if (scenario == "owner-form-history") {
|
|
if (owner_pending_history.ready()) {
|
|
if (!owner_history.commit(std::move(owner_pending_history))) {
|
|
fail("production-owner response history commit");
|
|
return;
|
|
}
|
|
owner_history_bytes = owner_history.current_has_request_data();
|
|
}
|
|
const bool retained_owner = NN::OwnerController::retain_active_link(
|
|
destination_hex, destination_hex,
|
|
active_link && active_link.status() == RNS::Type::Link::ACTIVE);
|
|
if (owner_requests < 3 &&
|
|
(!retained_owner || !active_link.pending_requests().empty())) {
|
|
fail("owner response did not retain active Link");
|
|
return;
|
|
}
|
|
if (owner_requests < 3) owner_retained_link = true;
|
|
if (owner_requests == 1) {
|
|
mailbox.prepare();
|
|
if (!actions.publish_submit(0, 2) ||
|
|
!service_submit_through_owner("Changed User")) {
|
|
fail("production-owner changed Submit");
|
|
}
|
|
break;
|
|
}
|
|
if (owner_requests == 2) {
|
|
NN::UserAction back_action;
|
|
back_action.kind = NN::UserActionKind::BACK;
|
|
ScreenSubmissionSource unused("unused");
|
|
auto back_command = owner.service(back_action, owner_history, unused, 0);
|
|
if (back_command.result != NN::OwnerResult::REQUEST ||
|
|
!back_command.pending_history.ready()) {
|
|
fail("production-owner Back history restore");
|
|
return;
|
|
}
|
|
if (back_command.request_data != owner_first_request) {
|
|
fail("production-owner Back exact request bytes");
|
|
return;
|
|
}
|
|
if (!owner_history.commit(std::move(back_command.pending_history)) ||
|
|
owner_history.current_request_data() != owner_first_request) {
|
|
fail("production-owner Back publication commit");
|
|
return;
|
|
}
|
|
owner_back_restored = true;
|
|
NN::UserAction reload_action;
|
|
reload_action.kind = NN::UserActionKind::RELOAD;
|
|
auto reload_command = owner.service(reload_action, owner_history, unused, 0);
|
|
mailbox.prepare();
|
|
owner_reload_reused = reload_command.result == NN::OwnerResult::REQUEST &&
|
|
reload_command.request_data == owner_first_request && owner_request_current();
|
|
if (!owner_reload_reused) fail("production-owner Reload request");
|
|
break;
|
|
}
|
|
if (owner_requests != 3 || link_callbacks != 1) {
|
|
fail("production-owner retained-Link Reload invariants");
|
|
return;
|
|
}
|
|
completed = true;
|
|
break;
|
|
}
|
|
if (scenario == "reuse" && reuse_requests == 1) {
|
|
if (!active_link || active_link.status() != RNS::Type::Link::ACTIVE ||
|
|
!active_link.pending_requests().empty()) {
|
|
fail("first reuse request did not conclude on active Link");
|
|
return;
|
|
}
|
|
mailbox.prepare();
|
|
active_link.set_resource_started_callback(on_resource_started);
|
|
const auto nil = NN::no_form_request_data();
|
|
receipt = active_link.request(RNS::Bytes("/page/reuse-second.mu"),
|
|
RNS::Bytes(nil.data(), nil.size()), on_response, on_failed,
|
|
on_progress, 8.0, NN::AsyncMailbox::MAX_WIRE_BYTES);
|
|
if (!receipt) {
|
|
fail("second request creation on retained Link");
|
|
return;
|
|
}
|
|
mailbox.expect_request(bytes_vector(receipt.get_request_id()));
|
|
++reuse_requests;
|
|
request_started_at = RNS::Utilities::OS::time();
|
|
break;
|
|
}
|
|
if (scenario == "reuse" &&
|
|
(reuse_requests != 2 || link_callbacks != 1 ||
|
|
!active_link.pending_requests().empty())) {
|
|
fail("same-destination Link reuse invariants");
|
|
return;
|
|
}
|
|
completed = true;
|
|
break;
|
|
}
|
|
case NN::AsyncMailbox::Kind::FAILED:
|
|
if (scenario != "timeout") {
|
|
fail("unexpected request failure");
|
|
return;
|
|
}
|
|
passed = true;
|
|
completed = true;
|
|
break;
|
|
case NN::AsyncMailbox::Kind::OVERSIZED:
|
|
std::printf("EVENT oversized transfer=%zu\n", event.transfer_size);
|
|
if (scenario == "oversized" && event.transfer_size > NN::AsyncMailbox::MAX_WIRE_BYTES) {
|
|
passed = true;
|
|
completed = true;
|
|
} else {
|
|
fail("unexpected oversized response");
|
|
}
|
|
break;
|
|
case NN::AsyncMailbox::Kind::LINK_CLOSED:
|
|
if (!cancellation_started && scenario != "timeout") fail("unexpected link close");
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void service_terminal_action() {
|
|
if (!actions.terminal_pending()) return;
|
|
NN::UserAction action;
|
|
if (!actions.pop(action) || action.kind != NN::UserActionKind::BACK) {
|
|
fail("terminal Back dequeue");
|
|
return;
|
|
}
|
|
cancellation_started = true;
|
|
cancellation_at = RNS::Utilities::OS::time();
|
|
mailbox.seal();
|
|
if (active_link && active_link.status() != RNS::Type::Link::CLOSED) active_link.teardown();
|
|
if (receipt && receipt.get_status() != RNS::Type::RequestReceipt::FAILED) {
|
|
receipt.request_timed_out(RNS::PacketReceipt(RNS::Type::NONE));
|
|
}
|
|
}
|
|
|
|
static void service_application_deadline() {
|
|
application_deadline_fired = true;
|
|
mailbox.seal();
|
|
if (active_link && active_link.status() != RNS::Type::Link::CLOSED) active_link.teardown();
|
|
if (receipt && receipt.get_status() != RNS::Type::RequestReceipt::FAILED) {
|
|
receipt.request_timed_out(RNS::PacketReceipt(RNS::Type::NONE));
|
|
}
|
|
}
|
|
|
|
static bool cleanup_complete() {
|
|
receipt_failed = receipt && receipt.get_status() == RNS::Type::RequestReceipt::FAILED;
|
|
pending_empty = active_link && active_link.pending_requests().empty();
|
|
link_closed = active_link && active_link.status() == RNS::Type::Link::CLOSED;
|
|
return receipt_failed && pending_empty && link_closed && stale_callback_rejections > 0;
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
if (argc == 2 && std::strcmp(argv[1], "--manifest") == 0) {
|
|
std::printf("{\"schema\":1,\"base\":\"%s\",\"branch\":\"%s\","
|
|
"\"microreticulum\":\"%s\",\"sources\":%s}\n",
|
|
PYXIS_MANIFEST_BASE, PYXIS_MANIFEST_BRANCH,
|
|
PYXIS_MANIFEST_MICRORETICULUM, PYXIS_MANIFEST_SOURCES_JSON);
|
|
return 0;
|
|
}
|
|
if (argc < 2) {
|
|
std::fprintf(stderr, "usage: %s immediate|resource|near-limit|oversized|timeout|cancel|reuse|form-anonymous|form-identified|owner-form-history|partial | lan host port destination\n", argv[0]);
|
|
return 2;
|
|
}
|
|
scenario = argv[1];
|
|
if (scenario != "immediate" && scenario != "resource" && scenario != "near-limit" &&
|
|
scenario != "oversized" &&
|
|
scenario != "timeout" && scenario != "cancel" && scenario != "reuse" &&
|
|
scenario != "form-anonymous" && scenario != "form-identified" &&
|
|
scenario != "owner-form-history" && scenario != "partial" && scenario != "lan") return 2;
|
|
if ((scenario == "lan" && argc != 5) || (scenario != "lan" && argc != 2)) return 2;
|
|
|
|
microStore::FileSystem filesystem{microStore::Adapters::UniversalFileSystem(".")};
|
|
filesystem.init();
|
|
RNS::Utilities::OS::register_filesystem(filesystem);
|
|
if (scenario == "lan") {
|
|
expected_destination_hex = argv[4];
|
|
tcp_interface = new TCPClientInterface("NomadNetLAN");
|
|
tcp_interface->set_target_host(argv[2]);
|
|
tcp_interface->set_target_port(std::stoi(argv[3]));
|
|
network_interface = RNS::Interface(tcp_interface);
|
|
} else {
|
|
network_interface = new UDPInterface();
|
|
network_interface.mode(RNS::Type::Interface::MODE_GATEWAY);
|
|
}
|
|
RNS::Transport::register_interface(network_interface);
|
|
if (!network_interface.start()) return 3;
|
|
reticulum = RNS::Reticulum();
|
|
reticulum.transport_enabled(false);
|
|
reticulum.start();
|
|
local_identity = RNS::Identity();
|
|
RNS::Transport::register_announce_handler(announce_handler);
|
|
if (scenario == "lan") {
|
|
RNS::Bytes target;
|
|
target.assignHex(expected_destination_hex.c_str());
|
|
RNS::Transport::request_path(target, network_interface);
|
|
std::printf("LAN TCP online=%d target=%s destination=%s path-requested=1\n",
|
|
network_interface.online() ? 1 : 0, argv[2], expected_destination_hex.c_str());
|
|
}
|
|
|
|
const double started_at = RNS::Utilities::OS::time();
|
|
while (!completed && RNS::Utilities::OS::time() - started_at < 15.0) {
|
|
// Mirrors the corrected firmware ordering: terminal cancellation gets
|
|
// an owner-loop chance before another inbound transport pass.
|
|
service_terminal_action();
|
|
reticulum.loop();
|
|
consume_event();
|
|
const double now = RNS::Utilities::OS::time();
|
|
if (request_started && scenario == "timeout" && !application_deadline_fired &&
|
|
now - request_started_at > 2.0) {
|
|
service_application_deadline();
|
|
}
|
|
const double cleanup_at = cancellation_started ? cancellation_at : request_started_at + 2.0;
|
|
if ((cancellation_started || application_deadline_fired) && cleanup_complete() &&
|
|
now - cleanup_at > 0.25) {
|
|
passed = scenario == "timeout" ||
|
|
(scenario == "cancel" && resource_started && resource_progress);
|
|
completed = true;
|
|
}
|
|
RNS::Utilities::OS::sleep(0.005);
|
|
}
|
|
|
|
mailbox.seal();
|
|
if (active_link && active_link.status() != RNS::Type::Link::CLOSED) active_link.teardown();
|
|
const double drain_until = RNS::Utilities::OS::time() + 0.25;
|
|
while (RNS::Utilities::OS::time() < drain_until) {
|
|
reticulum.loop();
|
|
RNS::Utilities::OS::sleep(0.005);
|
|
}
|
|
RNS::Transport::deregister_announce_handler(announce_handler);
|
|
RNS::Transport::deregister_interface(network_interface);
|
|
|
|
if (!completed) fail("scenario deadline");
|
|
cleanup_complete();
|
|
std::printf("RESULT scenario=%s announce=%d path=%d link=%d request=%d progress=%d callbacks=%d "
|
|
"cancel=%d deadline=%d resource_started=%d resource_progress=%d receipt_failed=%d "
|
|
"pending=%zu link_closed=%d stale_rejected=%d reuse_requests=%d link_callbacks=%d "
|
|
"owner_submit=%d history_bytes=%d retained_link=%d back_restored=%d reload_reused=%d partial_live=%d passed=%d\n",
|
|
scenario.c_str(), announce_seen ? 1 : 0,
|
|
destination_hex.empty() ? 0 : 1, link_established ? 1 : 0,
|
|
request_started ? 1 : 0, progress_seen ? 1 : 0, progress_callbacks,
|
|
cancellation_started ? 1 : 0, application_deadline_fired ? 1 : 0,
|
|
resource_started ? 1 : 0, resource_progress ? 1 : 0,
|
|
receipt_failed ? 1 : 0,
|
|
active_link ? active_link.pending_requests().size() : 0,
|
|
link_closed ? 1 : 0, stale_callback_rejections, reuse_requests, link_callbacks,
|
|
owner_submit ? 1 : 0, owner_history_bytes ? 1 : 0,
|
|
owner_retained_link ? 1 : 0, owner_back_restored ? 1 : 0,
|
|
owner_reload_reused ? 1 : 0, partial_live ? 1 : 0, passed ? 1 : 0);
|
|
if (scenario == "lan") {
|
|
std::printf("LAN TRANSPORT rx=%zu rxbytes=%zu tx=%zu txbytes=%zu\n",
|
|
network_interface.rx(), network_interface.rxbytes(),
|
|
network_interface.tx(), network_interface.txbytes());
|
|
}
|
|
return passed ? 0 : 1;
|
|
}
|