/* * Copyright (C) 2026 oxmc / PawletOS * SPDX-License-Identifier: Apache-2.0 */ #include "engine.h" #include #include #include #include "catalog.h" #include "detect.h" #include "http.h" #include "manifest.h" #include "pawlet/hwh_abi.h" #include "util.h" namespace pawlet::hwupdate { namespace { namespace fs = std::filesystem; bool Contains(const std::vector& v, const std::string& s) { return std::find(v.begin(), v.end(), s) != v.end(); } void ProgressCb(int percent, void* user) { const char* id = static_cast(user); LOG(INFO) << "hwupdate: flashing " << (id ? id : "?") << " " << percent << "%"; } } // namespace Engine::Engine(EngineConfig cfg) : cfg_(std::move(cfg)), state_(cfg_.stateDir + "/state.json") {} Engine::~Engine() { Stop(); } void Engine::Start() { std::error_code ec; fs::create_directories(cfg_.stateDir, ec); fs::create_directories(cfg_.stateDir + "/downloads", ec); worker_ = std::thread([this] { WorkerLoop(); }); CheckNow(); // initial cycle at boot } void Engine::Stop() { { std::lock_guard lk(reqMu_); if (stop_) return; stop_ = true; reqCv_.notify_all(); } if (worker_.joinable()) worker_.join(); } void Engine::CheckNow() { std::lock_guard lk(reqMu_); requested_ = true; reqCv_.notify_all(); } void Engine::WorkerLoop() { for (;;) { { std::unique_lock lk(reqMu_); reqCv_.wait(lk, [this] { return requested_ || stop_; }); if (stop_) return; requested_ = false; } busy_.store(true); int rc = RunCycle(); lastResult_.store(rc); busy_.store(false); } } int Engine::RunCycle() { auto manifest = LoadManifest(cfg_.manifestPath); if (!manifest) return -1; state_.Load(); std::vector detected = Detect(*manifest); std::vector comps; std::vector pend; for (auto& dc : detected) { const ManifestProduct* prod = dc.product; const ManifestComponent* comp = dc.comp; ComponentStatus cs; cs.componentId = comp->id; cs.productId = prod->id; cs.componentClass = comp->cls; cs.role = comp->role; cs.transport = dc.transport; cs.present = dc.present; cs.hardwareRevision = dc.hardwareRev; cs.support = comp->support; if (!dc.present) { comps.push_back(std::move(cs)); continue; } // Resolve a usable handler + read the installed version. const pawlet_hwh_api* handler = nullptr; for (const auto& hid : comp->installerHandlers) { handler = handlers_.Get(hid); if (handler) break; } pawlet_hw_ctx ctx{}; ctx.component_id = comp->id.c_str(); ctx.transport = dc.transport.c_str(); ctx.usb_vendor_id = dc.usbVendorId.c_str(); ctx.usb_product_id = dc.usbProductId.c_str(); ctx.dev_path = dc.devPath.c_str(); ctx.hardware_rev = dc.hardwareRev.c_str(); std::string installed = state_.InstalledVersion(comp->id); if (handler) { char ver[128] = {0}; if (handler->get_version(&ctx, ver, sizeof(ver)) == PAWLET_HWH_OK && ver[0]) { installed = ver; } } cs.installedVersion = installed; // Ask the server for the newest compatible update. CheckRequest req; req.device = manifest->device; req.pawletVersion = manifest->buildVersion; req.hardwareId = prod->id; req.componentId = comp->id; req.hardwareRevision = dc.hardwareRev; req.currentVersion = installed; auto update = CheckUpdate(cfg_.serverBaseUrl, req); PendingUpdate pu; pu.componentId = comp->id; if (update && update->version != installed) { pu.available = true; pu.version = update->version; pu.sizeBytes = update->sizeBytes; pu.changelogUrl = update->changelogUrl; // Trust boundary: the server-named handler MUST be allowlisted for // this component in the immutable manifest. if (!Contains(comp->installerHandlers, update->installerHandler)) { LOG(ERROR) << "hwupdate: refusing non-allowlisted handler '" << update->installerHandler << "' for " << comp->id; } else if (cfg_.autoApply) { if (state_.RetryCount(comp->id) >= cfg_.maxRetries) { LOG(WARNING) << "hwupdate: " << comp->id << " exceeded retries; skipping"; } else { const pawlet_hwh_api* flashHandler = handlers_.Get(update->installerHandler); std::string dl = cfg_.stateDir + "/downloads/" + update->id + "-" + update->filename; bool ok = flashHandler && http::Download(update->url, dl); if (ok) { std::string sum = util::Sha256File(dl); if (!util::IEquals(sum, update->checksumSha256)) { LOG(ERROR) << "hwupdate: checksum mismatch for " << comp->id; ok = false; } } if (ok) { int r = flashHandler->flash(&ctx, dl.c_str(), ProgressCb, const_cast(comp->id.c_str())); if (r == PAWLET_HWH_OK) { LOG(INFO) << "hwupdate: flashed " << comp->id << " -> " << update->version; state_.SetInstalledVersion(comp->id, update->version); state_.SetRetryCount(comp->id, 0); cs.installedVersion = update->version; pu.available = false; // applied } else { LOG(ERROR) << "hwupdate: flash failed rc=" << r << " for " << comp->id; state_.SetRetryCount(comp->id, state_.RetryCount(comp->id) + 1); ok = false; } } else { state_.SetRetryCount(comp->id, state_.RetryCount(comp->id) + 1); } std::error_code ec; fs::remove(dl, ec); } } } if (pu.available) pend.push_back(std::move(pu)); comps.push_back(std::move(cs)); } state_.Save(); { std::lock_guard lk(snapMu_); components_ = std::move(comps); pending_ = std::move(pend); } return 0; } std::vector Engine::Components() const { std::lock_guard lk(snapMu_); return components_; } std::vector Engine::Pending() const { std::lock_guard lk(snapMu_); return pending_; } } // namespace pawlet::hwupdate