/* StarForth — Steady-State Virtual Machine Runtime Copyright (c) 2023–2025 Robert A. James All rights reserved. This file is part of the StarForth project. Licensed under the StarForth License, Version 1.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: https://github.com/star.4th@proton.me/StarForth/LICENSE.txt This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and noninfringement. See the License for the specific language governing permissions and limitations under the License. */ /** * log_region.c - Growable per-VM log-persistence ring on Artemis's disk. * See starkernel/log_region.h for the format/interface design. * * Deliberately never calls log_message() (or anything documented to call * it) -- this runs inside Artemis's own dictionary context during message * delivery (LOG-APPEND, dispatched by a sender's MSG-SEND/MSG-TICK); a log * call here would append to Artemis's own buffer and eventually flush back * through this same path. */ #include "starkernel/log_region.h" #include #include #include "block_subsystem.h" /* blk_meta_zone_read/write, compute_crc64 */ static uint64_t log_region_ctrl_compute_crc(const log_region_ctrl_t *ctrl) { size_t crc_span = offsetof(log_region_ctrl_t, hdr_crc); return compute_crc64((const uint8_t *)ctrl, crc_span); } /* Reads the control header; returns 0 and *out populated only if magic, * version, and CRC all check out. Any other result (blank, foreign, * corrupt, or a read failure) is treated identically by the caller: "not * yet initialized," matching genesis_marker_read()'s own discipline. */ static int log_region_ctrl_read(log_region_ctrl_t *out) { if (blk_meta_zone_read(LOG_REGION_DEVBLOCK_FROM_TOP_BASE, (uint8_t *)out) != 0) return -1; if (LOG_REGION_GET_MAGIC(out->magic) != (uint32_t)(LOG_REGION_MAGIC & 0xFFFFFFFFull)) return -1; if (LOG_REGION_GET_VERSION(out->magic) != LOG_REGION_VERSION_0) return -1; uint64_t want_crc = log_region_ctrl_compute_crc(out); if (want_crc != out->hdr_crc) return -1; return 0; } static int log_region_ctrl_write(log_region_ctrl_t *ctrl) { ctrl->hdr_crc = log_region_ctrl_compute_crc(ctrl); return blk_meta_zone_write(LOG_REGION_DEVBLOCK_FROM_TOP_BASE, (const uint8_t *)ctrl) == 0 ? 0 : -1; } static void log_region_ctrl_init_fresh(log_region_ctrl_t *ctrl) { memset(ctrl, 0, sizeof(*ctrl)); ctrl->magic = LOG_REGION_PACK(LOG_REGION_VERSION_0); ctrl->devblocks = LOG_REGION_INITIAL_DEVBLOCKS; ctrl->head_slot = 0; ctrl->tail_slot = 0; ctrl->record_count = 0; } /* Reads/writes the devblock holding slot_index (LOG_SLOTS_PER_DEVBLOCK * slots per devblock), one devblock_from_top past the control header for * every LOG_SLOTS_PER_DEVBLOCK slots. */ static uint32_t log_region_slot_devblock_from_top(uint32_t slot_index) { return LOG_REGION_DEVBLOCK_FROM_TOP_BASE + 1u + (slot_index / LOG_SLOTS_PER_DEVBLOCK); } static int log_region_read_slot(uint32_t slot_index, log_slot_t *out) { uint8_t devblock_buf[4096]; uint32_t dft = log_region_slot_devblock_from_top(slot_index); uint32_t slot_in_devblock = slot_index % LOG_SLOTS_PER_DEVBLOCK; if (blk_meta_zone_read(dft, devblock_buf) != 0) return -1; memcpy(out, devblock_buf + (size_t)slot_in_devblock * LOG_SLOT_SIZE, sizeof(*out)); return 0; } static int log_region_write_slot(uint32_t slot_index, const log_slot_t *slot) { uint8_t devblock_buf[4096]; uint32_t dft = log_region_slot_devblock_from_top(slot_index); uint32_t slot_in_devblock = slot_index % LOG_SLOTS_PER_DEVBLOCK; /* Read-modify-write: blk_meta_zone_*() only operates at whole-devblock * granularity, and a devblock holds LOG_SLOTS_PER_DEVBLOCK slots, so a * single-slot write must preserve its siblings. A read failure on a * never-yet-written devblock (blank fence content) is not fatal here -- * proceed with a zeroed buffer, matching zuse_eligibility.c's own * "blank fence content is indistinguishable from absent, treat as * absent" discipline. */ if (blk_meta_zone_read(dft, devblock_buf) != 0) { memset(devblock_buf, 0, sizeof(devblock_buf)); } memcpy(devblock_buf + (size_t)slot_in_devblock * LOG_SLOT_SIZE, slot, sizeof(*slot)); return blk_meta_zone_write(dft, devblock_buf) == 0 ? 0 : -1; } int log_region_append(uint8_t level, uint64_t timestamp, const char *source, uint32_t source_len, const char *msg, uint32_t msg_len) { log_region_ctrl_t ctrl; if (log_region_ctrl_read(&ctrl) != 0) { log_region_ctrl_init_fresh(&ctrl); } uint32_t total_slots = ctrl.devblocks * LOG_SLOTS_PER_DEVBLOCK; if (ctrl.record_count >= total_slots) { if (ctrl.devblocks < LOG_REGION_MAX_DEVBLOCKS) { /* Grow: new slots appear at indices [old_total_slots .. * new_total_slots-1], nothing already written moves -- safe * to do at any point in the ring's lifecycle regardless of * wrap state, since head/tail are slot INDICES modulo * total_slots, and the modulus is simply larger from here on. */ ctrl.devblocks += LOG_REGION_GROWTH_INCREMENT; if (ctrl.devblocks > LOG_REGION_MAX_DEVBLOCKS) ctrl.devblocks = LOG_REGION_MAX_DEVBLOCKS; total_slots = ctrl.devblocks * LOG_SLOTS_PER_DEVBLOCK; } if (ctrl.record_count >= total_slots) { /* Still full (growth ceiling already reached) -- evict oldest, * unless doing so would discard higher-priority history for a * lower-priority newcomer (FABRIC-3.md §XXXII.4, 2026-09-15: * plain FIFO eviction here was flagged as level-blind -- a * flood of INFO/DEBUG noise could push out real ERROR/WARN * records). Read the oldest slot's own level before evicting: * if it's ERROR/WARN and the incoming record is not, protect * it -- drop the incoming record instead of evicting real * history. Any other combination (oldest is already low * priority, or the incoming record is itself ERROR/WARN) * evicts exactly as before. A read failure on the oldest slot * (should not happen -- it was written by this same function) * falls back to the prior unconditional-eviction behavior * rather than wedging the ring. */ log_slot_t oldest; int oldest_protected = 0; if (log_region_read_slot(ctrl.head_slot, &oldest) == 0) { oldest_protected = (oldest.level <= LOG_REGION_PROTECTED_MAX_LEVEL); } int incoming_protected = (level <= LOG_REGION_PROTECTED_MAX_LEVEL); if (oldest_protected && !incoming_protected) { return 1; /* dropped by design, not a failure -- see header */ } ctrl.head_slot = (ctrl.head_slot + 1u) % total_slots; ctrl.record_count--; } } log_slot_t slot; memset(&slot, 0, sizeof(slot)); slot.timestamp = timestamp; slot.level = level; uint32_t src_n = source_len; if (src_n > LOG_SLOT_SOURCE_MAX) src_n = LOG_SLOT_SOURCE_MAX; memcpy(slot.source, source, src_n); uint32_t msg_n = msg_len; if (msg_n > LOG_SLOT_MSG_MAX) msg_n = LOG_SLOT_MSG_MAX; memcpy(slot.msg, msg, msg_n); slot.msg_len = (uint16_t)msg_n; if (log_region_write_slot(ctrl.tail_slot, &slot) != 0) return -1; ctrl.tail_slot = (ctrl.tail_slot + 1u) % total_slots; if (ctrl.record_count < total_slots) ctrl.record_count++; return log_region_ctrl_write(&ctrl); }