freepdfeditor/spike/E_sandbox/UIProcess.cpp

119 lines
4.2 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
//
// UIProcess.cpp — the UI process side. Drives the Cap'n Proto RPC channel,
// sends Parse requests, and measures round-trip latency. The UI does no
// untrusted parsing; it validates the document process's responses (ADR-0004:
// bidirectional trust boundary).
//
// The client uses `EzRpcClient(int fd)`, which is designed for an
// already-connected socket (unlike EzRpcServer, which expects a listening
// socket). See DocumentProcess.cpp for the server-side integration note.
#include "UIProcess.h"
#include "ipc.capnp.h"
#include <capnp/ez-rpc.h>
#include <capnp/message.h>
#include <kj/array.h>
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
#include <unistd.h>
namespace proto = freepdfeditor::spike::e;
namespace freepdfeditor::spike::e {
UIResult run_ui_process(int parent_fd, int child_fd,
std::size_t n_requests, std::size_t payload_bytes)
{
(void)child_fd; // the child end is the document process's concern
UIResult r{};
// The payload the UI hands the document process. Sent as a Cap'n Proto
// Data field; the document process counts the bytes and returns the count.
std::vector<unsigned char> payload(payload_bytes, 0xAB);
// EzRpcClient(int fd) wraps an already-connected socket — exactly the
// socketpair-end shape. It sets up its own KJ EventLoop + WaitScope.
capnp::EzRpcClient client(parent_fd);
auto& waitScope = client.getWaitScope();
proto::DocumentProcess::Client doc =
client.getMain().castAs<proto::DocumentProcess>();
double min_us = 1e18, max_us = 0.0, sum_us = 0.0;
std::size_t acked = 0;
for (std::size_t i = 0; i < n_requests; ++i) {
auto t0 = std::chrono::steady_clock::now();
auto req = doc.parseRequest();
req.getRequest().setRequestId(static_cast<std::uint64_t>(i));
req.getRequest().setData(kj::ArrayPtr<const kj::byte>(
payload.data(), payload.size()));
// The Response owns the message backing the result reader, so it must
// stay alive for as long as we read from `resp` (a dangling reader is
// a use-after-free — ASan catches it, validating §7.2). Validate the
// response inside the scope that owns the Response.
bool ok = false;
std::string err;
std::uint64_t resp_id = 0;
std::uint64_t byte_count = 0;
try {
auto response = req.send().wait(waitScope);
auto resp = response.getResult();
ok = resp.getOk();
if (!ok) {
err = "doc returned ok=false: " + std::string(resp.getError().cStr());
} else {
resp_id = resp.getRequestId();
byte_count = resp.getByteCount();
}
} catch (kj::Exception& e) {
err = "RPC failed: " + std::string(e.getDescription().cStr());
}
auto t1 = std::chrono::steady_clock::now();
double us = std::chrono::duration<double, std::micro>(t1 - t0).count();
// Validate the response (ADR-0004: the IPC is a trust boundary in both
// directions — the UI never trusts the document process blindly).
if (!ok) {
r.error = err + " at " + std::to_string(i);
break;
}
if (resp_id != static_cast<std::uint64_t>(i)) {
r.error = "request id mismatch at " + std::to_string(i);
break;
}
if (byte_count != payload_bytes) {
r.error = "byte count mismatch at " + std::to_string(i) +
": got " + std::to_string(byte_count) +
" expected " + std::to_string(payload_bytes);
break;
}
sum_us += us;
min_us = std::min(min_us, us);
max_us = std::max(max_us, us);
++acked;
}
r.requests_sent = n_requests;
r.requests_acked = acked;
r.min_latency_us = acked ? min_us : 0.0;
r.max_latency_us = acked ? max_us : 0.0;
r.avg_latency_us = acked ? sum_us / double(acked) : 0.0;
r.total_time_us = sum_us;
r.ok = (acked == n_requests);
return r;
}
} // namespace freepdfeditor::spike::e