Read saved geocaches without online startup or GPX rehashing

This commit is contained in:
liu weikai
2026-09-28 17:41:11 +08:00
parent 2a30e2e0be
commit 175c8fca3d
5 changed files with 57 additions and 80 deletions
@@ -467,10 +467,13 @@ int main(int argc, char** argv)
const auto disk = test::files;
const auto disk_directories = test::directories;
std::fprintf(stderr, "Runtime: healthy restart\n");
// Cold offline startup has no dispatch destination or QueryClient yet.
// Saved rows and full details must not wait for either to become available.
router.ready = false;
test::source->activate(true);
until([&]
{ return std::strstr(snapshot(Section::Discover).status.data(), "Finding a public directory"); },
"restart did not restore indexed state");
{ return std::strstr(snapshot(Section::Discover).status.data(), "Waiting for Reticulum IP connection"); },
"offline startup did not wait for its online identity");
test::source->requestWindow(Section::Downloaded, 0, 4);
until([&]
{
@@ -478,7 +481,6 @@ int main(int argc, char** argv)
return view.count == 1 && test::source->item(Section::Downloaded, 0, view.generation, item) && item.id == saved_id && item.downloaded; },
"restart lost downloaded map row");
require(test::files == disk, "read-only restart changed persisted files");
router.ready = false;
const auto offline_sends = router.sends;
test::source->open(item, snapshot(Section::Downloaded).generation);
until([&]
@@ -490,6 +492,9 @@ int main(int argc, char** argv)
test::source->closeDetail();
tick();
router.ready = true;
until([&]
{ return std::strstr(snapshot(Section::Discover).status.data(), "Finding a public directory"); },
"offline session did not resume discovery when transport became ready");
test::source->requestWindow(Section::Published, 0, 4);
until([&]
{
@@ -2492,6 +2492,7 @@ int checkIndexedDownloadReceipt(const char* path)
if (read_saved(*indexed, {saved_id.data(), 32}, false, crypto) != DownloadRecoveryRead::End || owner.holder()) return 364;
if (read_saved(*indexed, {other_cache.bytes.data(), 32}, true, crypto) != DownloadRecoveryRead::End || owner.holder()) return 365;
const auto gpx_reads_before_listing = read_bytes[target];
SavedCacheCatalog<FileDigest> catalog(*indexed, crypto);
if (!owner.acquire(&other_owner) || catalog.advance() || !catalog.pending() || !owner.heldBy(&other_owner)) return 366;
owner.release(&other_owner);
@@ -2525,12 +2526,14 @@ int checkIndexedDownloadReceipt(const char* path)
!catalog.checked(other_cache.bytes, record.hash.bytes) || catalog.contains(other_cache.bytes, record.hash.bytes)) return 373;
files.at(target)[0] ^= 1;
catalog.reset();
if (!advance_catalog() || catalog.contains(record.id.bytes, record.hash.bytes) || !catalog.checked(record.id.bytes, record.hash.bytes)) return 374;
// Listing describes the installed record, without rereading the GPX.
// External file edits are checked before replacing that file.
if (!advance_catalog() || !catalog.contains(record.id.bytes, record.hash.bytes) || !catalog.checked(record.id.bytes, record.hash.bytes)) return 374;
files = disk;
catalog.requestWindow(0, 4);
if (!advance_catalog()) return 375;
catalog.snapshot(snapshot);
if (snapshot.count != 1) return 376;
if (snapshot.count != 1 || read_bytes[target] != gpx_reads_before_listing) return 376;
auto make_saved_reader = [&](size_t frame_capacity, size_t verify_capacity, protocol::RecordCrypto& verifier)
{
@@ -777,8 +777,8 @@ int checkDownloadController(const char* query_path, const char* response_path)
for (unsigned i = 0; saved.pending() && i < 256; ++i) saved.advance();
saved.snapshot(snapshot);
if (snapshot.count != 1 || saved.item(1, snapshot.generation, item)) return 342;
// Reset during hashing must be a UI-only notification. The next
// owner step discards the old reader and starts the new window.
// Reset is a UI-only notification. The next owner step starts
// reading metadata for the new window.
saved.requestWindow(0, 1);
saved.advance();
fixture::step_io_calls = 0;
@@ -805,7 +805,7 @@ int checkDownloadController(const char* query_path, const char* response_path)
saved.reset();
for (unsigned i = 0; saved.pending() && i < 256; ++i) saved.advance();
saved.snapshot(snapshot);
if (snapshot.count || !std::strstr(snapshot.status.data(), "changed")) return 186;
if (snapshot.count != 1 || !saved.item(0, snapshot.generation, item)) return 186;
fixture::files[target] = valid_file;
std::array<uint8_t, 4096> restored_a{}, restored_b{};
storage::LogicalState restored(restored_a.data(), restored_b.data(), restored_a.size());
@@ -1,16 +1,14 @@
#pragma once
#include "platform/esp/arduino_common/geocaching/download_store.h"
#include "platform/esp/arduino_common/geocaching/sd_gpx_hash.h"
#include "ui_presentation/geocaching/geocaching_source.h"
#include <cstdio>
#include <memory>
#include <new>
namespace platform::esp::arduino_common::geocaching
{
// Four owned rows shared by the saved-list window and discovery membership
// probes. UI calls never scan storage or close files. The maintenance owner
// advances verification and resets the projection after installs/media handoff.
// reads saved metadata and resets the projection after installs/media handoff.
// GPX integrity belongs to installation/recovery, not list rendering.
template <class Digest>
class SavedCacheCatalog
{
@@ -25,7 +23,6 @@ class SavedCacheCatalog
{
restart_ = true;
valid_ = 0;
invalid_ = false;
checking_ = true;
error_ = DownloadRecoveryRead::End;
++generation_;
@@ -36,11 +33,9 @@ class SavedCacheCatalog
// Maintenance-owner call before a foreground operation or media handoff.
void releaseRead()
{
if (!reader_ && !reading_ && !metadata_ready_) return;
if (!reading_) return;
if (reading_) store_.releaseRead();
reading_ = metadata_ready_ = false;
reader_.reset();
digest_.reset();
reading_ = false;
reset();
}
void requestWindow(size_t offset, size_t count)
@@ -87,21 +82,11 @@ class SavedCacheCatalog
if (restart_)
{
if (reading_) store_.releaseRead();
reader_.reset();
digest_.reset();
reading_ = metadata_ready_ = has_after_ = false;
reading_ = has_after_ = false;
cursor_ = total_ = 0;
restart_ = false;
}
if (!checking_) return false;
if (reader_)
{
if (reader_->step() == GpxHashStep::Reading) return true;
std::array<uint8_t, 32> hash;
finish(reader_->result(hash) && hash == current_.file_hash);
return true;
}
if (!metadata_ready_)
{
if (preview_)
{
@@ -132,31 +117,13 @@ class SavedCacheCatalog
++cursor_;
return true;
}
metadata_ready_ = true;
}
char path[112]{};
std::snprintf(path, sizeof(path), "/trailmate/geocaching/caches/");
size_t offset = std::strlen(path);
constexpr char hex[] = "0123456789abcdef";
for (auto byte : current_.id)
{
path[offset++] = hex[byte >> 4];
path[offset++] = hex[byte & 15];
}
std::memcpy(path + offset, ".gpx", 5);
digest_.reset(new (std::nothrow) Digest);
if (digest_) reader_.reset(new (std::nothrow) SdGpxHash<Digest>(*digest_));
if (!reader_)
{
digest_.reset();
return readError(DownloadRecoveryRead::Unavailable, true);
}
if (error_ != DownloadRecoveryRead::End)
{
error_ = DownloadRecoveryRead::End;
++generation_;
}
if (!reader_->open(path)) finish(false);
finish();
return true;
}
const char* error() const
@@ -183,8 +150,7 @@ class SavedCacheCatalog
out.generation = generation_;
out.can_refresh = !checking_;
out.count = count_;
std::snprintf(out.status.data(), out.status.size(), "%s", error() ? error() : checking_ ? "Checking saved GPX files..."
: invalid_ ? "Some GPX files changed or are unavailable"
std::snprintf(out.status.data(), out.status.size(), "%s", error() ? error() : checking_ ? "Loading downloaded caches..."
: out.count ? "Saved GPX - available offline"
: "No downloaded caches");
}
@@ -235,32 +201,23 @@ class SavedCacheCatalog
}
return false;
}
void finish(bool valid)
void finish()
{
if (valid)
const auto slot = preview_ ? requestedRow(current_.id, current_.hash) : total_ >= offset_ ? total_ - offset_
: rows_.size();
if (slot < window_)
{
const auto slot = preview_ ? requestedRow(current_.id, current_.hash) : total_ >= offset_ ? total_ - offset_
: rows_.size();
if (slot < window_)
{
rows_[slot] = current_;
valid_ |= uint8_t(1u << slot);
}
++total_;
rows_[slot] = current_;
valid_ |= uint8_t(1u << slot);
}
else invalid_ = true;
++total_;
after_ = current_.id;
has_after_ = true;
metadata_ready_ = false;
++cursor_;
++generation_;
reader_.reset();
digest_.reset();
}
DownloadStore& store_;
::geocaching::protocol::RecordCrypto& crypto_;
std::unique_ptr<Digest> digest_;
std::unique_ptr<SdGpxHash<Digest>> reader_;
std::array<uint8_t, 32> after_{};
std::array<::geocaching::storage::SavedCacheEntry, 4> rows_{};
::geocaching::storage::SavedCacheRecord current_;
@@ -268,7 +225,7 @@ class SavedCacheCatalog
size_t cursor_ = 0, total_ = 0, count_ = 0, offset_ = 0, window_ = 4;
uint8_t valid_ = 0, requested_ = 0;
DownloadRecoveryRead error_ = DownloadRecoveryRead::End;
bool checking_ = true, restart_ = false, invalid_ = false, preview_ = false;
bool reading_ = false, metadata_ready_ = false, has_after_ = false;
bool checking_ = true, restart_ = false, preview_ = false;
bool reading_ = false, has_after_ = false;
};
} // namespace platform::esp::arduino_common::geocaching
@@ -1826,7 +1826,7 @@ void step()
}
advanceBrowse(s);
if (s.detail) s.detail->advanceNetwork(*router, s.local, s.crypto, now(nullptr).monotonic_ms);
if (!s.storage_requested || !s.client)
if (!s.storage_requested)
{
next_step.store(millis() + 100);
return;
@@ -2126,20 +2126,11 @@ void step()
s.download_store.reset(new (std::nothrow) IndexedDownloadStore(s.volume, s.root, s.root_copy, s.roots[0], s.roots[1],
s.workspace_owner, s.workspace, s.frame, kFrameCapacity,
s.payload, kPayloadCapacity, s.verification, kVerificationCapacity, s.crypto));
s.dispatch_store.reset(new (std::nothrow) IndexedDispatchStore(s.volume, s.root, s.root_copy, s.roots[0], s.roots[1],
s.workspace_owner, s.workspace, s.frame, kFrameCapacity));
if (!s.store || !s.download_store || !s.dispatch_store || !s.workspace_owner.setPrepare(Session::prepareWorkspace, &s))
if (!s.store || !s.download_store || !s.workspace_owner.setPrepare(Session::prepareWorkspace, &s))
{
fail("Insufficient indexed storage memory");
return;
}
s.dispatcher.reset(new (std::nothrow) RequestDispatcher(*router, *s.dispatch_store, 5000, 120000));
s.receipts.reset(new (std::nothrow) StoredReplyReceipt(s.volume, s.root, s.local, s.workspace_owner, s.crypto));
if (!s.dispatcher || !s.receipts)
{
fail("Insufficient storage service memory");
return;
}
s.saved.reset(new (std::nothrow) SavedCacheCatalog<Digest>(*s.download_store, s.crypto));
if (!s.saved)
{
@@ -2164,8 +2155,8 @@ void step()
return;
}
// A user-edited installed GPX or missing history remains untouched.
// Its catalogue entry is checked separately; keep browsing other
// caches instead of turning one offline file into a service outage.
// Keep its installed metadata available for browsing instead of
// turning one offline file into a service outage.
s.recovery_attention |= result != gc::DownloadOperationResult::Complete || s.download_port->historyPending();
s.download_port.reset();
s.saved->reset();
@@ -2220,6 +2211,27 @@ void step()
fail("Storage interrupted - reopen to recover");
return;
}
// Local lists, details, drafts and interrupted file installation above
// do not need an online identity or request services. Only resume network
// work once the real local destination is known.
if (!s.client)
{
if (!startCheckpoint(s)) next_step.store(millis() + 100);
return;
}
if (!s.dispatch_store)
s.dispatch_store.reset(new (std::nothrow) IndexedDispatchStore(s.volume, s.root, s.root_copy, s.roots[0], s.roots[1],
s.workspace_owner, s.workspace, s.frame, kFrameCapacity));
if (s.dispatch_store && !s.dispatcher)
s.dispatcher.reset(new (std::nothrow) RequestDispatcher(*router, *s.dispatch_store, 5000, 120000));
if (!s.receipts)
s.receipts.reset(new (std::nothrow) StoredReplyReceipt(s.volume, s.root, s.local, s.workspace_owner, s.crypto));
if (!s.dispatcher || !s.receipts)
{
s.browse_status = "Waiting for network service memory";
next_step.store(millis() + 1000);
return;
}
if (publicationActive())
{
auto& job = *s.publication;