Files
ota-reference-design/examples/simulations/ab-trial-boot/simulation.cpp
T
2026-10-11 07:21:11 -04:00

234 lines
8.0 KiB
C++

#include "simulation.hpp"
#include <algorithm>
#include <fstream>
#include <iomanip>
#include <random>
#include <stdexcept>
#include <utility>
namespace ota {
namespace {
constexpr const char* phase_names[] = {
"A confirmed", "Prepare B", "B prepared and verified", "B trial pending",
"B trial running", "B confirmed", "Recovery to A pending",
"A recovery running", "A recovery checked", "Separate recovery required"
};
Phase read_phase(const std::string& value) {
for (int i = 0; i < 10; ++i) {
if (value == phase_names[i]) return static_cast<Phase>(i);
}
throw std::runtime_error("Invalid saved phase");
}
Slot read_slot(const std::string& value) {
if (value == "A") return Slot::A;
if (value == "B") return Slot::B;
throw std::runtime_error("Invalid saved slot");
}
void require(bool condition, const char* message) {
if (!condition) throw std::logic_error(message);
}
} // namespace
const char* name(Phase phase) { return phase_names[static_cast<int>(phase)]; }
const char* name(Slot slot) { return slot == Slot::A ? "A" : "B"; }
Device::Device(const std::filesystem::path& directory)
: path_(directory / "boot-state.txt") {
if (!std::filesystem::exists(path_)) {
save();
return;
}
std::ifstream input(path_);
std::string header, phase, normal, trial;
int a_ready = 0, b_ready = 0;
if (!(input >> header >> std::quoted(phase) >> normal >> trial
>> state_.attempts_left >> a_ready >> b_ready
>> std::quoted(state_.outcome)) || header != "OTA_AB_STATE_V1") {
throw std::runtime_error("Unreadable saved boot state");
}
input >> std::ws;
if (!input.eof() || state_.attempts_left < 0 ||
(a_ready != 0 && a_ready != 1) || (b_ready != 0 && b_ready != 1)) {
throw std::runtime_error("Invalid saved boot state");
}
state_.phase = read_phase(phase);
state_.normal = read_slot(normal);
if (trial != "none") state_.trial = read_slot(trial);
state_.a_ready = a_ready != 0;
state_.b_ready = b_ready != 0;
}
void Device::save() const {
auto temporary = path_;
temporary += ".tmp";
std::ofstream output(temporary, std::ios::trunc);
output << "OTA_AB_STATE_V1\n" << std::quoted(name(state_.phase)) << '\n'
<< name(state_.normal) << '\n'
<< (state_.trial ? name(*state_.trial) : "none") << '\n'
<< state_.attempts_left << '\n'
<< state_.a_ready << ' ' << state_.b_ready << '\n'
<< std::quoted(state_.outcome) << '\n';
output.close();
if (!output) throw std::runtime_error("Cannot save boot state");
// Teaching stand-in: rename alone does not guarantee power-loss durability.
std::filesystem::rename(temporary, path_);
}
void Device::prepare(bool complete) {
require(state_.phase == Phase::AConfirmed || state_.phase == Phase::PreparingB,
"Cannot prepare B in this phase");
state_.b_ready = false;
state_.phase = Phase::PreparingB;
save();
if (complete) {
// Represents complete installation AND artifact verification.
state_.b_ready = true;
state_.phase = Phase::BPrepared;
save();
}
}
void Device::request_trial(int attempts) {
require(state_.phase == Phase::BPrepared && state_.b_ready,
"B must be prepared and verified before trial");
require(attempts > 0, "The trial needs at least one attempt");
state_.phase = Phase::TrialPending;
state_.trial = Slot::B;
state_.attempts_left = attempts;
save();
}
Slot Device::boot() {
require(state_.phase != Phase::SeparateRecoveryRequired,
"Separate recovery is required; do not alternate failed systems");
if (state_.trial) {
if (state_.attempts_left > 0) {
--state_.attempts_left;
state_.phase = Phase::TrialRunning;
// Persist the consumed attempt BEFORE handing control to B.
save();
running_ = *state_.trial;
return *running_;
}
state_.phase = Phase::RecoveryPending;
state_.trial.reset();
save();
}
if (state_.phase == Phase::RecoveryPending) {
state_.phase = Phase::RecoveryRunning;
save();
}
running_ = state_.normal;
return *running_;
}
void Device::check(bool healthy) {
require(running_.has_value(), "Boot before checking the running system");
require(state_.phase == Phase::TrialRunning || state_.phase == Phase::RecoveryRunning,
"Checks require a running trial or recovery");
if (state_.phase == Phase::TrialRunning) {
state_.trial.reset();
state_.attempts_left = 0;
if (healthy) {
state_.phase = Phase::BConfirmed;
state_.normal = Slot::B;
state_.outcome = "B confirmed";
} else {
state_.phase = Phase::RecoveryPending;
}
} else if (healthy) {
state_.phase = Phase::RecoveryChecked;
state_.outcome = "Recovered to A";
} else {
state_.phase = Phase::SeparateRecoveryRequired;
state_.outcome = "Recovery A failed";
}
save();
}
TemporaryDirectory::TemporaryDirectory() {
std::random_device random;
for (int attempt = 0; attempt < 100; ++attempt) {
auto candidate = std::filesystem::temp_directory_path() /
("ota-ab-simulation-" + std::to_string(random()) + "-" + std::to_string(random()));
if (std::filesystem::create_directory(candidate)) {
path_ = std::move(candidate);
return;
}
}
throw std::runtime_error("Cannot create a unique temporary directory");
}
TemporaryDirectory::~TemporaryDirectory() {
std::error_code ignored;
std::filesystem::remove_all(path_, ignored);
}
const std::vector<std::string>& scenarios() {
static const std::vector<std::string> names = {
"success", "health-failure", "attempts-exhausted", "restart-during-trial",
"interrupted-preparation", "recovery-failure"
};
return names;
}
Device run_scenario(const std::string& scenario, const std::filesystem::path& directory,
std::ostream* output) {
if (std::find(scenarios().begin(), scenarios().end(), scenario) == scenarios().end()) {
throw std::invalid_argument("Unknown scenario: " + scenario);
}
Device device(directory);
auto show = [&](const std::string& event) {
if (!output) return;
const auto& state = device.state();
*output << event << ": " << name(state.phase) << "; normal=" << name(state.normal)
<< "; attempts_left=" << state.attempts_left << "; running="
<< (device.running() ? name(*device.running()) : "none") << '\n';
};
auto reset = [&] {
device = Device(directory);
const auto selected = device.boot();
show(std::string("Reset -> ") + name(selected));
};
auto message = [&](const char* text) { if (output) *output << text << '\n'; };
show("Initial state");
if (scenario == "interrupted-preparation") {
device.prepare(false);
show("Interrupted installation: B remains ineligible");
reset();
return device;
}
device.prepare();
show("Installation and verification complete");
device.request_trial(2);
show("Trial requested");
reset();
if (scenario == "attempts-exhausted" || scenario == "restart-during-trial") {
message("No confirmation: simulate a reset caused by power loss or watchdog");
reset();
}
if (scenario == "attempts-exhausted") {
message("B still cannot reach health checks; simulate another reset");
reset();
device.check(true);
show("Recovery checks pass");
} else if (scenario == "health-failure" || scenario == "recovery-failure") {
device.check(false);
show("B health checks fail");
reset();
device.check(scenario != "recovery-failure");
show("Recovery checks complete");
} else {
device.check(true);
show("B health checks pass; confirmation saved");
reset();
}
return device;
}
} // namespace ota