Public Access
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
83 lines
3.3 KiB
C++
83 lines
3.3 KiB
C++
#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. Certificates are trusted on first use, per host and port (Q71).
|
|
class GeminiService {
|
|
public:
|
|
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 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
|