Public Access
Updates: a download that breaks is carried on
When the connection breaks during a release download (issue #54), the device waits up to a minute for Wi-Fi, asks for the rest of the file with a Range request and carries on: the slot and the hash so far are kept. It gives up after three tries in a row that bring nothing. Nothing is kept across a restart. Carrying on asks for 64 KB free, not 80: between two connections of an install 78 KB is free, and the first attempt on the device was refused for that. update damage cut now breaks every connection after n bytes. Tried on the device: v0.23.0 installed over four connections, and a download that brought nothing given up with nothing switched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0162FokPdvY2KsS4NBfwyWPk
This commit is contained in:
+3
-1
@@ -579,6 +579,7 @@ static void updateCommand(const String& args) {
|
||||
console.printf("update: installing by itself %s%s%s\n", settings.getBool(Setting::InstallUpdates) ? "on" : "off",
|
||||
autoUpdate.waiting() ? ", waiting for a quiet moment to offer " : autoUpdate.asking() ? ", asking about " : "",
|
||||
autoUpdate.waiting() || autoUpdate.asking() ? autoUpdate.tag().c_str() : "");
|
||||
console.printf("update: the last download was carried on %d time(s) after a break\n", update->lastResumes());
|
||||
console.printf("update: gitea %s %s\n", g.status() == GiteaReleases::Status::Done ? "done" : g.status() == GiteaReleases::Status::Failed ? "failed" : g.status() == GiteaReleases::Status::Busy ? "busy" : "never asked", g.error().c_str());
|
||||
printReleases(false);
|
||||
} else if (args.startsWith("install ")) {
|
||||
@@ -594,7 +595,8 @@ static void updateCommand(const String& args) {
|
||||
} else if (args.startsWith("damage ")) { // update damage cut <bytes> | flip <offset>: for the next download
|
||||
long n = args.substring(args.lastIndexOf(' ') + 1).toInt();
|
||||
args.startsWith("damage cut") ? update->damageNextDownload(n, -1) : update->damageNextDownload(-1, n);
|
||||
console.printf("update: the next download will be %s at byte %ld\n", args.startsWith("damage cut") ? "cut" : "damaged", n);
|
||||
if (args.startsWith("damage cut")) console.printf("update: each connection of the next download will break after %ld bytes\n", n);
|
||||
else console.printf("update: the next download will be damaged at byte %ld\n", n);
|
||||
|
||||
} else if (args == "daily") { // forget today's check: the daily one runs again at once, if it may
|
||||
update->forgetToday();
|
||||
|
||||
@@ -27,9 +27,20 @@ std::string whyNotConnected(NetworkClientSecure& tls, const std::string& host) {
|
||||
} // namespace
|
||||
|
||||
std::string HttpsGet::open(const std::string& host, const std::string& path, const char* accept) {
|
||||
std::string why = request(host, path, accept, "", kNeedFree);
|
||||
if (!why.empty()) return why;
|
||||
if (head_.status != 200) return host + " answered " + std::to_string(head_.status) + (head_.status / 100 == 3 ? " (a redirect)" : "");
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string HttpsGet::openFrom(const std::string& host, const std::string& path, const char* accept, long from, const std::string& ifRange) {
|
||||
return request(host, path, accept, "Range: bytes=" + std::to_string(from) + "-\r\n" + (ifRange.empty() ? "" : "If-Range: " + ifRange + "\r\n"), kNeedFreeToCarryOn);
|
||||
}
|
||||
|
||||
std::string HttpsGet::request(const std::string& host, const std::string& path, const char* accept, const std::string& more, size_t needFree) {
|
||||
close();
|
||||
if (time(nullptr) < kClockSetAfter) return "The clock isn't set: can't check certificates";
|
||||
if (esp_get_free_heap_size() < kNeedFree) return "Not enough memory for a secure connection";
|
||||
if (esp_get_free_heap_size() < needFree) return "Not enough memory for a secure connection";
|
||||
|
||||
tls_.setCACert(kTrustedRootsPem);
|
||||
tls_.setTimeout(15);
|
||||
@@ -38,7 +49,7 @@ std::string HttpsGet::open(const std::string& host, const std::string& path, con
|
||||
raw_.reset(new (std::nothrow) uint8_t[kRaw]);
|
||||
if (!raw_) return "Not enough memory";
|
||||
tls_.print(("GET " + path + " HTTP/1.1\r\nHost: " + host + "\r\nUser-Agent: roro9stack/" + versionString() +
|
||||
"\r\nAccept: " + accept + "\r\nAccept-Encoding: identity\r\nConnection: close\r\n\r\n")
|
||||
"\r\nAccept: " + accept + "\r\nAccept-Encoding: identity\r\n" + more + "Connection: close\r\n\r\n")
|
||||
.c_str());
|
||||
|
||||
release::HttpHeadParser parser;
|
||||
@@ -50,7 +61,6 @@ std::string HttpsGet::open(const std::string& host, const std::string& path, con
|
||||
if (parser.complete() && used < static_cast<size_t>(n)) early_.assign(reinterpret_cast<const char*>(raw_.get()) + used, n - used);
|
||||
}
|
||||
head_ = parser.head();
|
||||
if (head_.status != 200) return host + " answered " + std::to_string(head_.status) + (head_.status / 100 == 3 ? " (a redirect)" : "");
|
||||
left_ = head_.chunked ? -1 : head_.contentLength;
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -20,12 +20,22 @@ class HttpsGet {
|
||||
// checking the certificate); with Q86's 20 KB to spare, it starts with at least this much free.
|
||||
static constexpr size_t kNeedFree = 80 * 1024;
|
||||
|
||||
// Carrying on a download (issue #54) happens in the middle of an install, whose own buffers
|
||||
// (about 8 KB, measured: 78 KB free between two connections) are already taken out of what is
|
||||
// free, and with the screen taken over nothing else will ask for memory: the peak and 12 KB.
|
||||
static constexpr size_t kNeedFreeToCarryOn = 64 * 1024;
|
||||
|
||||
explicit HttpsGet(net::User user) : tls_(user) {}
|
||||
~HttpsGet() { close(); }
|
||||
|
||||
// Connects and reads the head. "" when a 200 is on its way, or why not, in words for the screen.
|
||||
std::string open(const std::string& host, const std::string& path, const char* accept);
|
||||
// The same, for the rest of a file from byte `from` (issue #54): asks for that range, with
|
||||
// `ifRange` (an ETag, or "") so that a file that changed comes whole. The answer, a 206 or
|
||||
// not, is for the caller to judge with head(): "" here only says a head was read.
|
||||
std::string openFrom(const std::string& host, const std::string& path, const char* accept, long from, const std::string& ifRange);
|
||||
long contentLength() const { return head_.contentLength; } // -1: not known
|
||||
const release::HttpHead& head() const { return head_; }
|
||||
|
||||
// Body bytes into `buf`: how many, 0 at the end, -1 when the connection broke or stalled
|
||||
// before the end.
|
||||
@@ -37,6 +47,7 @@ class HttpsGet {
|
||||
static constexpr uint32_t kStallMs = 10000;
|
||||
|
||||
int readRaw(uint8_t* into, size_t len); // from the connection, waiting up to kStallMs
|
||||
std::string request(const std::string& host, const std::string& path, const char* accept, const std::string& more, size_t needFree);
|
||||
|
||||
Counted<NetworkClientSecure> tls_;
|
||||
release::HttpHead head_;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <ctime>
|
||||
|
||||
#include "http_head.h"
|
||||
#include "resume.h"
|
||||
#include "platform/https_get.h"
|
||||
#include "platform/ota_device.h"
|
||||
#include "platform/system_info.h"
|
||||
@@ -64,21 +65,61 @@ class FileSource : public UpdateSource {
|
||||
File& f_;
|
||||
};
|
||||
|
||||
// A release being downloaded from Gitea: the same bytes a push or a card would give.
|
||||
// A release being downloaded from Gitea: the same bytes a push or a card would give. When the
|
||||
// connection breaks, it is opened again for the rest of the file (issue #54): what was written
|
||||
// to the slot stays, and the parser never sees the break.
|
||||
class GiteaSource : public UpdateSource {
|
||||
public:
|
||||
GiteaSource(HttpsGet& get, long cutAfter, long flipAt) : get_(get), cut_(cutAfter), flip_(flipAt) {}
|
||||
static constexpr uint32_t kWifiWaitMs = 60000; // for Wi-Fi to come back, before each try
|
||||
static constexpr uint32_t kPauseMs = 2000;
|
||||
|
||||
GiteaSource(HttpsGet& get, const release::Url& url, WifiService& wifi, long cutAfter, long flipAt)
|
||||
: get_(get), url_(url), wifi_(wifi), total_(get.contentLength()), etag_(get.head().etag), 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;
|
||||
for (;;) {
|
||||
// Debug: each connection "breaks" after cut_ bytes.
|
||||
int n = cut_ >= 0 && pos_ - openedAt_ >= cut_ ? -1 : 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;
|
||||
if (n >= 0) return n;
|
||||
if (!resume()) return -1;
|
||||
}
|
||||
}
|
||||
int resumes() const { return policy_.resumes(); }
|
||||
const std::string& why() const { return why_; } // why it gave up, when it did
|
||||
|
||||
private:
|
||||
bool resume() {
|
||||
// Without a length there is no knowing what "the rest" is.
|
||||
while (total_ > 0 && pos_ < total_ && policy_.again(pos_)) {
|
||||
get_.close();
|
||||
delay(kPauseMs);
|
||||
uint32_t since = millis();
|
||||
while (wifi_.state() != WifiController::State::Connected && millis() - since < kWifiWaitMs) delay(250);
|
||||
if (wifi_.state() != WifiController::State::Connected) {
|
||||
why_ = "Wi-Fi did not come back";
|
||||
continue;
|
||||
}
|
||||
why_ = get_.openFrom(url_.host, url_.path, "application/octet-stream", pos_, release::ifRangeFor(etag_));
|
||||
if (!why_.empty()) continue; // no connection yet: another try, while the policy allows
|
||||
why_ = release::resumeRefusal(get_.head(), pos_, total_, etag_);
|
||||
if (!why_.empty()) return false; // it answered, and it is not the rest of this file
|
||||
openedAt_ = pos_;
|
||||
ESP_LOGW("update", "carrying on from byte %ld of %ld (try %d)", pos_, total_, policy_.resumes());
|
||||
return true;
|
||||
}
|
||||
if (why_.empty()) why_ = "The download broke";
|
||||
return false;
|
||||
}
|
||||
|
||||
HttpsGet& get_;
|
||||
long cut_, flip_, pos_ = 0;
|
||||
release::Url url_;
|
||||
WifiService& wifi_;
|
||||
long total_;
|
||||
std::string etag_, why_;
|
||||
release::ResumePolicy policy_;
|
||||
long cut_, flip_, pos_ = 0, openedAt_ = 0;
|
||||
};
|
||||
|
||||
constexpr time_t kClockSetAfter = 1700000000; // anything earlier is the clock's default
|
||||
@@ -363,9 +404,12 @@ void UpdateService::installFromGitea(const release::Release& r) {
|
||||
notify("Update refused: " + why, NotificationLevel::Warning);
|
||||
return;
|
||||
}
|
||||
GiteaSource source(get, damageCut_, damageFlip_);
|
||||
GiteaSource source(get, url, wifi_, damageCut_, damageFlip_);
|
||||
damageCut_ = damageFlip_ = -1;
|
||||
install(source, "Gitea");
|
||||
resumes_ = source.resumes();
|
||||
// What the parser says after a break is only that the file was short: say why it broke.
|
||||
if (phase_ != Phase::Installed && !source.why().empty()) notify("Download gave up: " + source.why(), NotificationLevel::Warning);
|
||||
}
|
||||
|
||||
void UpdateService::taskEntry(void* self) { static_cast<UpdateService*>(self)->listen(); }
|
||||
|
||||
@@ -71,6 +71,7 @@ class UpdateService : public Service {
|
||||
// (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_; }
|
||||
int lastResumes() const { return resumes_; } // how often the last download from Gitea was carried on (issue #54)
|
||||
void damageNextDownload(long cutAfter, long flipAt) { damageCut_ = cutAfter; damageFlip_ = flipAt; }
|
||||
void forgetToday() { store_.putInt("rel_day", 0); nextCheckMs_ = 0; announced_.clear(); } // the daily check runs at the next tick
|
||||
|
||||
@@ -115,6 +116,7 @@ class UpdateService : public Service {
|
||||
uint32_t nextCheckMs_ = 90000; // not before the device has settled
|
||||
std::string probeHost_, probePath_;
|
||||
volatile long damageCut_ = -1, damageFlip_ = -1;
|
||||
volatile int resumes_ = 0;
|
||||
std::string probeResult_;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user