234 lines
8.0 KiB
C++
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
|