Files
LithosAnanake/src/starkernel/hal/console.c
T
Robert Allan JamesandClaude Sonnet 5 b0f12710bb Console-VM + user-VM pair: real async message-passing relay
Console sessions now route through the same general VM-to-VM messaging
system (Phase C) any VM can already use for its own reasons -- not a
synchronous shortcut. Per direct instruction: real async MSG-SEND/
MSG-DELIVER (Option B), not a VM-EXEC-based synchronous relay, because
messaging is a general capability, not a console-specific mechanism.

New CONSOLE-CMD-EVENT message type (common:messaging.4th). New
sk_repl_dispatch_line() (repl.c), called from both sk_repl_step and
sk_repl_run in place of a direct vm_interpret(): if the active VM's own
name has a live "<name>~user" counterpart registered, the raw input
line is wrapped as an S"-embedded CONSOLE-CMD-EVENT MSG-SEND and
interpreted on the console VM instead of being run directly -- the
console's own next MSG-TICK (Hera's idle pump) delivers it into the
paired user VM via VM-EXEC, same mechanism every other message already
uses. Falls back to direct interpretation if there's no pairing, or if
the line contains a `"` (known v1 limitation, warned about explicitly
rather than silently mishandled).

New capsule_console_birth() (capsule_console.h/.c): a bare VM whose
only content is loading common:messaging.4th -- the console side of a
pairing, parallel in shape to RUNCAP's user-VM birth but with fixed
embedded content instead of a devblock read (no identity, no thumbdrive
involved). New PAIR-TEST diagnostic word (mama_forth_words.c, matches
RUNCAP-TEST's own precedent): births both halves of a pairing and
registers the "<name>~user" mapping. Not the real pairing call site --
that's the eventual attach/onboarding flow -- this exists to exercise
the relay live before that flow exists.

Found and fixed a real, serious bug live: console_set_vm_name() stored
the caller's raw pointer instead of copying it. mama_word_use() (USE)
passes a VMRegistryEntry field living on its own stack frame -- once
USE returns, that pointer dangles, corrupting every console tag after
the first USE (observed directly as garbled "[[]" / binary-looking
prefixes instead of "[CaptBob]"). Fixed at the source: console_set_
vm_name() now copies into internal storage. That surfaced a second,
related bug across every console_get_vm_name()-based save/restore call
site in mama_forth_words.c (BIRTH, VM-STEP, VM-EXEC, CONNECT-HERMES,
CONNECT-ARTEMIS): saving just a pointer into the single internal buffer
meant an intervening console_set_vm_name() call silently corrupted the
saved value before the restore ever ran. New console_save_vm_name()
copies into caller-owned storage; every save/restore site updated.

Verified end-to-end, live in QEMU: typed WELCOME at a paired console
VM -- it did not execute directly (no UNKNOWN WORD), printed ok
immediately (queued, async), and on the next idle tick
"[CaptBob~user] Minted identity -- default personality" appeared on
its own -- genuine delivery and execution in the paired user VM through
the real MSG-SEND/MSG-DELIVER pipeline. Console tags confirmed clean
(no garbling) across all three architectures' full regression boot.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZGkimpfyh63EZyRkNbkPD
2026-08-28 16:39:07 -04:00

391 lines
11 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 20232025 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.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 20232025 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.
*/
/**
* console.c - Serial console implementation (UART 16550)
* Supports amd64 via I/O ports
*/
#include "console.h"
#include "framebuffer.h"
#include "vt100.h"
#include "arch.h"
#if defined(__x86_64__) || defined(__i386__)
#define SERIAL_SUPPORTED 1
/* UART 16550 I/O ports (COM1) */
#define SERIAL_PORT_BASE 0x3F8
#define SERIAL_DATA_PORT (SERIAL_PORT_BASE + 0)
#define SERIAL_INT_ENABLE_PORT (SERIAL_PORT_BASE + 1)
#define SERIAL_FIFO_CTRL_PORT (SERIAL_PORT_BASE + 2)
#define SERIAL_LINE_CTRL_PORT (SERIAL_PORT_BASE + 3)
#define SERIAL_MODEM_CTRL_PORT (SERIAL_PORT_BASE + 4)
#define SERIAL_LINE_STATUS_PORT (SERIAL_PORT_BASE + 5)
/* Line Status Register bits */
#define SERIAL_LSR_DATA_READY (1 << 0)
#define SERIAL_LSR_THR_EMPTY (1 << 5)
static inline void outb(uint16_t port, uint8_t val) {
__asm__ volatile ("outb %0, %1" : : "a"(val), "Nd"(port));
}
static inline uint8_t inb(uint16_t port) {
uint8_t ret;
__asm__ volatile ("inb %1, %0" : "=a"(ret) : "Nd"(port));
return ret;
}
#elif defined(__aarch64__)
#define SERIAL_SUPPORTED 1
/* PL011 UART — QEMU virt machine UART0 at 0x09000000 */
#define PL011_BASE ((volatile unsigned int *)0x09000000UL)
#define PL011_DR (PL011_BASE + 0) /* Data Register (offset 0x000) */
#define PL011_FR (PL011_BASE + 6) /* Flag Register (offset 0x018) */
#define PL011_FR_TXFF (1u << 5) /* TX FIFO full */
static inline void pl011_putc(char c)
{
while (*PL011_FR & PL011_FR_TXFF) { }
*PL011_DR = (unsigned int)(unsigned char)c;
}
#elif defined(__riscv)
#define SERIAL_SUPPORTED 1
/* NS16550 UART — QEMU virt machine UART0 at 0x10000000 (MMIO, byte-wide) */
#define NS16550_BASE ((volatile unsigned char *)0x10000000UL)
#define NS16550_THR (NS16550_BASE + 0) /* Transmit Holding Register */
#define NS16550_LSR (NS16550_BASE + 5) /* Line Status Register */
#define NS16550_LSR_THRE (1u << 5) /* Transmit Holding Register Empty */
#define NS16550_LSR_DR (1u << 0) /* Data Ready */
static inline void ns16550_putc(char c)
{
while (!(*NS16550_LSR & NS16550_LSR_THRE)) { }
*NS16550_THR = (unsigned char)c;
}
#else
#define SERIAL_SUPPORTED 0
/* Stubs for other architectures */
static inline void outb(uint16_t port, uint8_t val) {
(void)port;
(void)val;
}
static inline uint8_t inb(uint16_t port) {
(void)port;
return 0;
}
#endif
/**
* Initialize serial console
* x86: programs 16550 UART.
* aarch64: PL011 is already initialized by UEFI firmware on QEMU virt.
*/
void console_init(void) {
#if defined(__x86_64__) || defined(__i386__)
outb(SERIAL_INT_ENABLE_PORT, 0x00);
outb(SERIAL_LINE_CTRL_PORT, 0x80);
outb(SERIAL_DATA_PORT, 0x01);
outb(SERIAL_INT_ENABLE_PORT, 0x00);
outb(SERIAL_LINE_CTRL_PORT, 0x03);
outb(SERIAL_FIFO_CTRL_PORT, 0xC7);
outb(SERIAL_MODEM_CTRL_PORT, 0x0B);
outb(SERIAL_MODEM_CTRL_PORT, 0x1E);
outb(SERIAL_DATA_PORT, 0xAE);
if (inb(SERIAL_DATA_PORT) != 0xAE) {
return;
}
outb(SERIAL_MODEM_CTRL_PORT, 0x0F);
#elif defined(__aarch64__)
/* PL011 already enabled by AAVMF; nothing to do */
(void)0;
#elif defined(__riscv)
/* NS16550 already enabled by UEFI firmware on QEMU virt; nothing to do */
(void)0;
#else
(void)SERIAL_SUPPORTED;
#endif
}
#if defined(__x86_64__) || defined(__i386__)
static int serial_transmit_empty(void) {
return inb(SERIAL_LINE_STATUS_PORT) & SERIAL_LSR_THR_EMPTY;
}
#endif
/* Active VM name for [Name] line prefix; NULL = no prefix.
*
* g_active_vm_name_buf owns the storage -- console_set_vm_name() copies
* into it rather than storing the caller's own pointer. Found live
* 2026-08-28 (FABRIC-3.md Phase F): mama_word_use() (USE) passes
* entry.name, a local VMRegistryEntry's own field -- once USE returns,
* that stack frame is reused and the old raw-pointer version left
* g_active_vm_name dangling, corrupting every console tag after the
* first USE (observed as garbled "[[]"/binary-looking prefixes). A
* caller passing a string literal (e.g. console_set_vm_name("Hermes"))
* was always safe; this fixes every caller uniformly instead of relying
* on each one happening to pass static storage. */
#define CONSOLE_VM_NAME_BUF 64
static char g_active_vm_name_buf[CONSOLE_VM_NAME_BUF];
static const char *g_active_vm_name = (void *)0;
static int g_line_start = 1;
void console_set_vm_name(const char *name) {
/* Empty string treated the same as NULL: console_save_vm_name()
* writes "" for "there was no active name," so this keeps that
* round-trip correct (save-empty then restore-empty must mean
* "still no prefix," not "prefix is now the empty string"). */
if (!name || !name[0]) { g_active_vm_name = (void *)0; return; }
size_t i;
for (i = 0; i < CONSOLE_VM_NAME_BUF - 1u && name[i]; i++)
g_active_vm_name_buf[i] = name[i];
g_active_vm_name_buf[i] = '\0';
g_active_vm_name = g_active_vm_name_buf;
}
const char *console_get_vm_name(void) {
return g_active_vm_name;
}
void console_save_vm_name(char *out, size_t cap) {
if (!out || cap == 0) return;
size_t i = 0;
if (g_active_vm_name) {
for (; i < cap - 1u && g_active_vm_name[i]; i++)
out[i] = g_active_vm_name[i];
}
out[i] = '\0';
}
/* Raw single-character write — no prefix logic, called by emit_prefix() */
static void raw_putc(char c) {
#if defined(__aarch64__)
if (c == '\n') pl011_putc('\r');
pl011_putc(c);
#elif defined(__riscv)
if (c == '\n') ns16550_putc('\r');
ns16550_putc(c);
#elif defined(__x86_64__) || defined(__i386__)
if (c == '\n') {
while (!serial_transmit_empty()) { arch_relax(); }
outb(SERIAL_DATA_PORT, '\r');
}
while (!serial_transmit_empty()) { arch_relax(); }
outb(SERIAL_DATA_PORT, (uint8_t)c);
#else
(void)c;
#endif
}
/* Emit "[VMName] " to both serial (raw_putc) and, when available, the
* framebuffer (vt100_putc) -- mirrors console_putc()'s own serial/framebuffer
* split so the prefix reaches both outputs, not serial only. No recursion
* into console_putc itself (would re-trigger the line-start prefix check). */
/* FABRIC.md 4.4: the bracketed VM name (brackets included) renders in
* standard web orange, 0xFFA500 -- not in the classic 16-color ANSI
* palette, so sent as a literal 24-bit SGR sequence rather than a palette
* index. Same dual serial+framebuffer send pattern as the rest of this
* function. */
static void emit_prefix(void) {
const char *p;
int fb = fb_is_available();
static const char *color_on = "\x1b[38;2;255;165;0m";
static const char *color_off = "\x1b[39m";
for (p = color_on; *p; p++) { raw_putc(*p); if (fb) vt100_putc(*p); }
raw_putc('[');
if (fb) vt100_putc('[');
for (p = g_active_vm_name; *p; p++) {
raw_putc(*p);
if (fb) vt100_putc(*p);
}
raw_putc(']');
if (fb) vt100_putc(']');
for (p = color_off; *p; p++) { raw_putc(*p); if (fb) vt100_putc(*p); }
raw_putc(' ');
if (fb) vt100_putc(' ');
}
/**
* Write a single character to serial console.
* Emits "[VMName] " at the start of each new line when a VM name is set.
* Also mirrors output to the framebuffer VT100 terminal when available.
* Serial output is ALWAYS active regardless of framebuffer state.
*/
void console_putc(char c) {
/* --- serial UART path (always on) --- */
if (g_active_vm_name && g_line_start && c != '\n') {
emit_prefix();
g_line_start = 0;
}
raw_putc(c);
if (c == '\n') {
g_line_start = 1;
}
/* --- framebuffer VT100 path (when available) --- */
if (fb_is_available()) {
if (c == '\n') vt100_putc('\r');
vt100_putc(c);
}
}
/**
* Write a null-terminated string to serial console
*/
void console_puts(const char *s) {
if (!s) return;
while (*s) {
console_putc(*s++);
}
}
/**
* Write a string with newline to serial console
*/
void console_println(const char *s) {
console_puts(s);
console_putc('\n');
}
/**
* Check if data is available to read
*/
int console_poll(void) {
#if defined(__x86_64__) || defined(__i386__)
return inb(SERIAL_LINE_STATUS_PORT) & SERIAL_LSR_DATA_READY;
#elif defined(__aarch64__)
/* PL011_FR bit 4 = RXFE (RX FIFO empty); data available when RXFE == 0 */
#define PL011_FR_RXFE (1u << 4)
return !(*PL011_FR & PL011_FR_RXFE);
#elif defined(__riscv)
return *NS16550_LSR & NS16550_LSR_DR;
#else
return 0;
#endif
}
/**
* Initialize the framebuffer VT100 terminal and attach it to the console.
* Safe to call with info == NULL or when the framebuffer base is zero (no-op).
* After this call every console_putc / console_puts output is mirrored to
* the screen; the serial UART continues to function concurrently.
*/
void console_fb_init(const FramebufferInfo *info, FbPixelFormat fmt)
{
if (!info || !info->base) return;
fb_init(info, fmt);
if (fb_is_available()) {
vt100_init();
}
}
void console_fb_enable_ttf(void)
{
if (fb_is_available()) {
vt100_enable_ttf();
}
}
void console_fb_scroll_back(uint32_t n)
{
if (fb_is_available()) {
vt100_scroll_back(n);
}
}
void console_fb_scroll_fwd(uint32_t n)
{
if (fb_is_available()) {
vt100_scroll_fwd(n);
}
}
void console_fb_toggle_graphics(void)
{
if (fb_is_available()) {
vt100_toggle_graphics();
}
}
void console_fb_draw_cursor(void)
{
if (fb_is_available()) {
vt100_draw_cursor();
}
}
void console_fb_erase_cursor(void)
{
if (fb_is_available()) {
vt100_erase_cursor();
}
}
/**
* Read a single character from serial console (non-blocking)
* Returns -1 if no character available
*/
int console_getc(void) {
#if defined(__x86_64__) || defined(__i386__)
if (!console_poll()) return -1;
return (int)(unsigned char)inb(SERIAL_DATA_PORT);
#elif defined(__aarch64__)
if (!console_poll()) return -1;
return (int)(*PL011_DR & 0xFF);
#elif defined(__riscv)
if (!console_poll()) return -1;
return (int)(*NS16550_THR & 0xFF);
#else
return -1;
#endif
}