// 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 #include #include #include #include #include #include #include #include #include #include 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 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(); 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(i)); req.getRequest().setData(kj::ArrayPtr( 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(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(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