Updates from Gitea, step 3: install from Gitea, and IRC steps aside for it

A release downloads straight into the inactive slot through the existing
install path: the signature is checked after 160 bytes, before anything
is written, the hash at the end. Tried on the device against the real
server: a download cut short, a flipped byte in the signature and one in
the image are each refused with the running firmware untouched; the real
v0.10.0 installed, restarted, and confirmed itself on Probation.

A TLS connection to Gitea peaks at about 52 KB of heap whether or not the
certificate is verified. With IRC connected (66 KB free) a check left 3 KB
and a download 836 bytes. A check, list or install a person asks for now
makes IRC step aside (holdForUpdate) and come back after: the lowest free
heap during a full download with IRC connected is 38 KB. The daily check
never interrupts IRC; with IRC up it waits. A TLS connection starts with
80 KB free (it was 55).

Debug Builds get test knobs: update probe <host>, update damage cut|flip,
update pretend <version>.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
2026-10-06 15:51:10 +02:00
co-authored by Claude Sonnet 5.5
parent 5754ae5b57
commit 510ce42a99
10 changed files with 120 additions and 10 deletions
+1
View File
@@ -24,6 +24,7 @@ class GiteaReleases {
bool fetchLatest();
bool fetchList();
void fail(const std::string& error) { finish(false, error); } // something stopped it before it began
Status status() const;
std::string error() const;
uint32_t seq() const; // grows with every answer that changed something
+8
View File
@@ -224,6 +224,14 @@ void IrcService::loop() {
if (!wanted_) {
if (open_) close("disconnected");
status_ = Status::Stopped;
} else if (held_) {
if (open_) {
conn_->print("QUIT :updating\r\n");
vTaskDelay(pdMS_TO_TICKS(300));
close("paused while the device updates");
}
status_ = Status::Paused;
retryAtMs_ = now; // back at once when released
} else if (!wifiUp) {
if (open_) close(monitoring ? "paused for Wi-Fi monitoring" : "Wi-Fi lost");
status_ = monitoring ? Status::Paused : Status::WaitingForWifi;
+6
View File
@@ -38,6 +38,11 @@ class IrcService : public Service {
void disconnect();
bool running() const { return wanted_; }
bool stoppedByUser() const { return stoppedByUser_; }
// A TLS connection to Gitea needs more memory than is left beside this one (#6, Q172): the
// Update Service asks IRC to step aside while it talks to the server, and to come back after.
// Unlike disconnect(), nothing is remembered as "stopped by the user".
void holdForUpdate(bool hold) { held_ = hold; }
Status status() const { return status_; }
// Read or act on the session while holding its lock: withSession([](IrcSession& s) { ... }).
@@ -86,6 +91,7 @@ class IrcService : public Service {
volatile bool stopRequested_ = false;
bool quitSent_ = false; // /quit already queued its QUIT
volatile bool restart_ = false;
volatile bool held_ = false; // stepping aside for an update
volatile Status status_ = Status::Stopped;
bool open_ = false;
uint32_t retryAtMs_ = 0;
+49 -6
View File
@@ -67,15 +67,21 @@ class FileSource : public UpdateSource {
// A release being downloaded from Gitea: the same bytes a push or a card would give.
class GiteaSource : public UpdateSource {
public:
explicit GiteaSource(HttpsGet& get) : get_(get) {}
int read(uint8_t* buf, size_t len) override { return get_.read(buf, len); }
GiteaSource(HttpsGet& get, long cutAfter, long flipAt) : get_(get), cut_(cutAfter), flip_(flipAt) {}
int read(uint8_t* buf, size_t len) override {
if (cut_ >= 0 && pos_ >= cut_) return -1; // Debug Builds: the connection "breaks" here
int n = get_.read(buf, len);
if (n > 0 && flip_ >= pos_ && flip_ < pos_ + n) buf[flip_ - pos_] ^= 1; // and a byte "arrives wrong"
if (n > 0) pos_ += n;
return n;
}
private:
HttpsGet& get_;
long cut_, flip_, pos_ = 0;
};
constexpr time_t kClockSetAfter = 1700000000; // anything earlier is the clock's default
constexpr size_t kCheckFloor = 55 * 1024; // Q86, Q172
} // namespace
@@ -106,7 +112,7 @@ void UpdateService::start() {
store_.putString("ota_pending", "");
}
if (!task_) xTaskCreate(taskEntry, "update", 8192, this, 1, &task_); // TLS to Gitea (#6): measured below
if (!task_) xTaskCreate(taskEntry, "update", 7168, this, 1, &task_); // peak 5.4 KB: TLS to Gitea and the signature check (#6)
}
void UpdateService::bootGuard(KeyValueStore& store) {
@@ -235,7 +241,8 @@ void UpdateService::scheduleDailyCheck(uint32_t nowMs) {
nextCheckMs_ = nowMs + 3600000; // look again in an hour, in case the day has turned
return;
}
if (esp_get_free_heap_size() < kCheckFloor) {
// Never at IRC's expense: with IRC connected there isn't the room (Q172), so this waits.
if (esp_get_free_heap_size() < HttpsGet::kNeedFree) {
nextCheckMs_ = nowMs + 600000;
return;
}
@@ -243,11 +250,31 @@ void UpdateService::scheduleDailyCheck(uint32_t nowMs) {
request_ = Request::BackgroundCheck;
}
// What a person asked for (a check, a list, an install) may take IRC offline for a few seconds:
// a TLS connection needs about 80 KB and IRC's own holds 40 KB of what there is (R1, Q172).
bool UpdateService::makeRoom() {
if (esp_get_free_heap_size() >= HttpsGet::kNeedFree) return true;
if (!holdMemory) return false;
held_ = true;
holdMemory(true);
for (int i = 0; i < 40 && esp_get_free_heap_size() < HttpsGet::kNeedFree; i++) delay(100); // its TLS session goes
return esp_get_free_heap_size() >= HttpsGet::kNeedFree;
}
void UpdateService::serve() {
Request r = request_;
if (r == Request::None) return;
request_ = Request::None;
serving_ = true;
held_ = false;
if (r != Request::BackgroundCheck && r != Request::None) {
bool ok = makeRoom();
if (!ok) {
gitea_.fail("Not enough memory: close a Gemini page");
if (r == Request::Install) notify("Update refused: not enough memory", NotificationLevel::Warning);
r = Request::None;
}
}
switch (r) {
case Request::Check:
case Request::BackgroundCheck: {
@@ -268,8 +295,19 @@ void UpdateService::serve() {
if (gitea_.find(installTag_, release)) installFromGitea(release);
break;
}
#ifdef RORO_DEBUG
case Request::Probe: {
HttpsGet get(net::User::Updates);
std::string why = get.open(probeHost_, probePath_, "*/*");
probeResult_ = why.empty() ? "accepted, the server answered 200" : why;
break;
}
#endif
case Request::None: break;
}
// IRC comes back, unless the device is about to restart into an update.
if (held_ && phase_ != Phase::Installed && holdMemory) holdMemory(false);
held_ = false;
serving_ = false;
}
@@ -287,7 +325,12 @@ void UpdateService::installFromGitea(const release::Release& r) {
notify("Update refused: " + why, NotificationLevel::Warning);
return;
}
GiteaSource source(get);
#ifdef RORO_DEBUG
GiteaSource source(get, damageCut_, damageFlip_);
damageCut_ = damageFlip_ = -1;
#else
GiteaSource source(get, -1, -1);
#endif
install(source, "Gitea");
}
+19 -1
View File
@@ -2,6 +2,7 @@
#include <freertos/FreeRTOS.h>
#include <functional>
#include <string>
#include "update_check.h"
@@ -53,12 +54,22 @@ class UpdateService : public Service {
// it would take its Debug Console away) unless `force`, which only the Debug Console passes.
std::string requestInstall(const std::string& tag, bool force = false);
bool giteaBusy() const { return request_ != Request::None || serving_; }
// Asked to make room for a TLS connection (R1, Q172): IRC steps aside while the Update Service
// talks to Gitea. Set by the main loop; `true` to step aside, `false` to come back.
std::function<void(bool)> holdMemory;
// The daily check (Q165) only runs once the main loop says it may: not in Safe Mode.
void enableDailyCheck() { dailyCheck_ = true; }
// The version that counts as running for comparisons: a Debug Build can pretend to be older.
std::string runningVersion() const { return fakeVersion_.empty() ? versionString() : fakeVersion_; }
void pretendVersion(const std::string& v) { fakeVersion_ = v; }
std::string failedVersion() const; // a release that rolled back here, "" if none
#ifdef RORO_DEBUG
// Debug Builds only, to try the refusals on the real network path: one HTTPS GET to any host
// (is its certificate accepted?), and a download cut short or with one byte flipped.
void requestProbe(const std::string& host, const std::string& path) { probeHost_ = host; probePath_ = path; probeResult_ = "..."; request_ = Request::Probe; }
std::string probeResult() const { return probeResult_; }
void damageNextDownload(long cutAfter, long flipAt) { damageCut_ = cutAfter; damageFlip_ = flipAt; }
#endif
// The main loop calls this once it has drawn a frame (part of Probation).
void firstFrameDrawn() { firstFrame_ = true; }
@@ -66,9 +77,10 @@ class UpdateService : public Service {
bool rebootPending() const { return phase_ == Phase::Installed; }
private:
enum class Request : uint8_t { None, Check, BackgroundCheck, List, Install };
enum class Request : uint8_t { None, Check, BackgroundCheck, List, Install, Probe };
void serve(); // on this task: one request
bool makeRoom(); // true when a TLS connection can start; may have asked holdMemory
void installFromGitea(const release::Release& release);
void scheduleDailyCheck(uint32_t nowMs); // from tick()
static void taskEntry(void* self);
@@ -92,9 +104,15 @@ class UpdateService : public Service {
GiteaReleases gitea_;
volatile Request request_ = Request::None;
volatile bool serving_ = false;
bool held_ = false; // IRC was asked to step aside, on this task
std::string installTag_, fakeVersion_, announced_;
bool dailyCheck_ = false;
uint32_t nextCheckMs_ = 90000; // not before the device has settled
#ifdef RORO_DEBUG
std::string probeHost_, probePath_;
volatile long damageCut_ = -1, damageFlip_ = -1;
std::string probeResult_;
#endif
};
} // namespace roro