Public Access
G1 step 3: the Gemini fetcher (TOFU, redirects, floors, pages via the card)
GeminiService fetches on a short-lived task and hands the App a GeminiPage: header, the body as lines in 4 KB chunks (TextBuffer: no large block, no doubling copies), the final URL after up to 5 redirects. Certificates are pinned on first use per host and port; a change comes back as its own outcome with both fingerprints. No fetch starts below 55 KB free (Q86). With a card, the body streams to /gemini/cache/page.gmi in 1 KB pieces while the connection is open, then loads into RAM once its memory is back (Q87); StorageService::runAndWait (moved from the Debug Console) keeps every card access on the storage task. Without a card: RAM, with the steady and transient floors. Measured with IRC connected: Cosmos (31.6 KB) went from 4.6 KB to the whole page on the card and 20 KB on screen; lowest free heap 19.5 KB in transfer, 43 KB once loaded. `gemini get` and `gemini trust` on the console. 14 Gemini tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -1,21 +1,82 @@
|
||||
#pragma once
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "gemini_response.h"
|
||||
#include "key_value_store.h"
|
||||
#include "services/storage_service.h"
|
||||
#include "text_buffer.h"
|
||||
|
||||
class NetworkClientSecure;
|
||||
|
||||
namespace roro {
|
||||
|
||||
// What a fetch came back with.
|
||||
struct GeminiPage {
|
||||
enum class Outcome { Ok, Failed, CertificateChanged };
|
||||
Outcome outcome = Outcome::Failed;
|
||||
std::string requested; // what was asked for
|
||||
std::string url; // where it ended up, after redirects
|
||||
gemini::Header header; // the final response's header
|
||||
gemini::TextBuffer text; // 2x only: the body as lines, up to kMaxBody
|
||||
bool truncated = false; // the body was cut: see truncatedWhy
|
||||
std::string truncatedWhy;
|
||||
std::string error; // Failed: why, for a human
|
||||
std::string host; // CertificateChanged: whose, and both fingerprints
|
||||
int port = 1965;
|
||||
std::string pinned, presented;
|
||||
std::string cachePath; // with a card: the whole body as received (Saved Pages copy it)
|
||||
size_t receivedBytes = 0; // the whole body, which may be more than `text` holds
|
||||
uint32_t ms = 0;
|
||||
};
|
||||
|
||||
// Gemini (docs/milestones/G1.md). Each fetch runs on a short-lived task of its own: a TLS
|
||||
// handshake can block for seconds, which the main loop's watchdog wouldn't allow, and an idle
|
||||
// client should cost no stack.
|
||||
// client should cost no stack. Certificates are trusted on first use, per host and port (Q71).
|
||||
class GeminiService {
|
||||
public:
|
||||
// `gemini get <url>`: fetches and prints the header, size, certificate fingerprint, the first
|
||||
// lines and the heap around the fetch to the console. False if a fetch is already running.
|
||||
static constexpr size_t kMaxBody = 64 * 1024; // Q74
|
||||
static constexpr size_t kStartFloor = 55 * 1024; // Q86: no fetch below this
|
||||
static constexpr size_t kSteadyFloor = 40 * 1024; // Q86: what must be left once the page is in
|
||||
static constexpr size_t kTransientFloor = 20 * 1024; // Q86: while the TLS connection is open
|
||||
static constexpr int kMaxRedirects = 5; // Q72
|
||||
|
||||
static constexpr size_t kMaxOnCard = 1024 * 1024; // a text page larger than this is cut
|
||||
static constexpr const char* kCachePath = "/gemini/cache/page.gmi";
|
||||
|
||||
GeminiService(KeyValueStore& store, StorageService& storage);
|
||||
|
||||
// Starts a fetch; false if one is running. The result comes from takeResult().
|
||||
bool fetch(const std::string& url);
|
||||
bool busy() const { return busy_; }
|
||||
bool takeResult(GeminiPage& out);
|
||||
void cancel() { cancel_ = true; }
|
||||
// After the user accepts a changed certificate.
|
||||
void trust(const std::string& host, int port, const std::string& fingerprint);
|
||||
|
||||
// `gemini get <url>`: the same fetch, reported on the console instead of to the App.
|
||||
bool fetchToConsole(const std::string& url);
|
||||
|
||||
private:
|
||||
static void consoleTask(void* arg);
|
||||
volatile bool busy_ = false;
|
||||
static void taskEntry(void* self);
|
||||
void run();
|
||||
void fetchOne(const std::string& url, GeminiPage& page);
|
||||
void report(const GeminiPage& page, size_t heapBefore, size_t heapLowest);
|
||||
std::string pinKey(const std::string& host, int port) const;
|
||||
|
||||
bool receiveToCard(::NetworkClientSecure& tls, const char* first, size_t len, GeminiPage& page);
|
||||
void loadFromCard(GeminiPage& page, size_t freeBefore);
|
||||
|
||||
KeyValueStore& store_;
|
||||
StorageService& storage_;
|
||||
SemaphoreHandle_t lock_;
|
||||
std::string pendingUrl_;
|
||||
GeminiPage result_;
|
||||
volatile bool busy_ = false, ready_ = false, cancel_ = false, toConsole_ = false;
|
||||
size_t lowest_ = 0;
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
|
||||
Reference in New Issue
Block a user