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
391 lines
11 KiB
C
391 lines
11 KiB
C
/*
|
||
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.
|
||
|
||
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.
|
||
|
||
*/
|
||
|
||
/**
|
||
* 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
|
||
}
|