#include "simulation.hpp" #include #include #include #include #include #include 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(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(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& scenarios() { static const std::vector 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