53#include <system_error>
76 if (::kill(
static_cast<::pid_t
>(pid), 0) == 0)
return true;
77 return errno == EPERM;
88 const std::string& stem) {
89 namespace fs = std::filesystem;
91 if (!fs::is_directory(dir, ec))
return;
92 const std::string prefixes[2] = {stem +
".initial.", stem +
".reload."};
93 for (fs::directory_iterator it(dir, ec),
end; !ec && it !=
end; it.increment(ec)) {
94 const std::string fn = it->path().filename().string();
95 std::size_t pos = std::string::npos;
96 for (
const auto& p : prefixes)
97 if (fn.rfind(p, 0) == 0) { pos = p.size();
break; }
98 if (pos == std::string::npos)
continue;
99 const std::size_t dot = fn.find(
'.', pos);
100 if (dot == std::string::npos || dot == pos)
continue;
102 const std::string pid_str = fn.substr(pos, dot - pos);
103 try { pid = std::stol(pid_str); }
catch (...) {
continue; }
106 fs::remove(it->path(), rm);
115 std::filesystem::path stage_dir = {},
117 : session_(session), path_(
std::move(logic_path)),
118 stage_dir_(stage_dir.empty() ? path_.parent_path() :
std::move(stage_dir)),
119 trust_policy_(
std::move(trust_policy)) {
129 std::optional<ReloadOutcome>
poll() {
130 const auto mtime = current_mtime();
131 if (!mtime)
return std::nullopt;
132 if (!baseline_set_) {
133 last_mtime_ = *mtime;
134 if (
const auto h = hash_file(path_)) { last_hash_ = *h; hash_valid_ =
true; }
135 baseline_set_ =
true;
138 if (*mtime == last_mtime_)
return std::nullopt;
139 last_mtime_ = *mtime;
148 const auto hash = hash_file(path_);
149 if (!hash)
return std::nullopt;
150 if (hash_valid_ && *hash == last_hash_)
return std::nullopt;
153 return stage_and_reload();
159 if (
const auto mtime = current_mtime()) {
160 last_mtime_ = *mtime;
161 baseline_set_ =
true;
163 if (
const auto hash = hash_file(path_)) {
167 return stage_and_reload();
170 const std::filesystem::path&
path()
const {
return path_; }
176 std::filesystem::file_time_type
mtime{};
180 if (!baseline_set_ || !hash_valid_)
182 return Baseline{last_mtime_, last_hash_};
191 last_mtime_ = b.mtime;
194 baseline_set_ =
true;
198 static std::optional<std::uint64_t>
content_hash(
const std::filesystem::path& p) {
203 std::optional<std::filesystem::file_time_type> current_mtime()
const {
205 const auto t = std::filesystem::last_write_time(path_, ec);
206 if (ec)
return std::nullopt;
215 static std::optional<std::uint64_t> hash_file(
const std::filesystem::path& p) {
216 std::ifstream f(p, std::ios::binary);
217 if (!f)
return std::nullopt;
218 std::uint64_t h = 1469598103934665603ULL;
219 std::array<char, 64 * 1024> buf{};
220 std::size_t total = 0;
222 f.read(buf.data(),
static_cast<std::streamsize
>(buf.size()));
223 const std::streamsize got = f.gcount();
224 for (std::streamsize i = 0; i < got; ++i) {
225 h ^=
static_cast<unsigned char>(buf[
static_cast<std::size_t
>(i)]);
226 h *= 1099511628211ULL;
228 total +=
static_cast<std::size_t
>(got);
230 if (
f.bad() || total == 0)
return std::nullopt;
236 static std::optional<std::string> sha256_of_file(
const std::filesystem::path& p) {
237 std::ifstream
f(p, std::ios::binary);
238 if (!f)
return std::nullopt;
239 const std::string bytes((std::istreambuf_iterator<char>(f)),
240 std::istreambuf_iterator<char>());
247 ReloadOutcome stage_and_reload() {
257 std::string expected_signed_sha;
258 if (trust_policy_.require_signed) {
260 if (
auto* refused = std::get_if<ReloadOutcome>(&decision))
return *refused;
261 if (
auto* trust = std::get_if<SwapPackTrust>(&decision)) {
264 const std::string member = path_.filename().string();
265 for (
const auto& f : trust->manifest.files)
266 if (
f.path == member) { expected_signed_sha =
f.sha256_hex;
break; }
267 if (expected_signed_sha.empty())
269 "require_signed: watched file is not a named member of its "
282 static std::atomic<std::uint64_t> global_counter{0};
285 static_cast<long>(::_getpid());
287 static_cast<long>(::getpid());
291 (path_.stem().
string() +
".reload." + std::to_string(pid) +
"." +
292 std::to_string(global_counter.fetch_add(1, std::memory_order_relaxed)) +
293 path_.extension().string());
295 std::filesystem::copy_file(
296 path_, staged, std::filesystem::copy_options::overwrite_existing, ec);
299 "stage copy failed: " + ec.message()};
305 if (!expected_signed_sha.empty()) {
306 const auto staged_sha = sha256_of_file(staged);
307 if (!staged_sha || *staged_sha != expected_signed_sha) {
309 std::filesystem::remove(staged, rm);
311 "require_signed: staged bytes do not match the signed hash "
312 "(source changed after verification) — refused"};
323 if (!last_staged_.empty() && last_staged_ != staged) {
324 std::error_code rm_ec;
325 std::filesystem::remove(last_staged_, rm_ec);
327 last_staged_ = staged;
328 return session_.reload(staged.string());
331 ReloadSession& session_;
332 std::filesystem::path path_;
333 std::filesystem::path stage_dir_;
334 ReloadTrustPolicy trust_policy_;
335 std::filesystem::file_time_type last_mtime_{};
336 std::filesystem::path last_staged_;
337 std::uint64_t last_hash_ = 0;
338 bool hash_valid_ =
false;
339 bool baseline_set_ =
false;
340 std::uint64_t attempts_ = 0;
@ f
Definition pitch.hpp:25
std::string sha256_hex(const uint8_t *data, size_t size)
Compute SHA-256 hash as hex string (64 chars).
@ rejected
Definition rmlui_host_bridge.hpp:21
@ h
Definition input_events.hpp:39
The verify-before-commit reload transaction — ties the Phase-0 primitives into one operation: load a ...