feat(rumpk): Implement PTY subsystem for terminal semantics
Phase 40: The Soul Bridge IMPLEMENTED: - PTY subsystem with master/slave fd pairs (100-107 / 200-207) - Ring buffer-based bidirectional I/O (4KB each direction) - Line discipline (CANON/RAW modes, echo support) - Integration with FB terminal renderer CHANGES: - [NEW] core/pty.nim - Complete PTY implementation - [MODIFY] kernel.nim - Wire PTY to syscalls, add pty_init() to boot DATA FLOW: Keyboard → ION chan_input → pty_push_input → master_to_slave buffer → pty_read_slave → mksh stdin → mksh stdout → pty_write_slave → term_putc/term_render → Framebuffer VERIFICATION: [PTY] Subsystem Initialized [PTY] Allocated ID=0x0000000000000000 [PTY] Console PTY Allocated REMAINING: /dev/tty device node for full TTY support Co-authored-by: <voxis@nexus-os.org>
This commit is contained in:
parent
4cec2d8c25
commit
72891287fb
|
|
@ -22,8 +22,11 @@ import fs/tar
|
||||||
import fs/sfs
|
import fs/sfs
|
||||||
import fs/vfs
|
import fs/vfs
|
||||||
import netswitch
|
import netswitch
|
||||||
|
import ../libs/membrane/libc
|
||||||
import ../libs/membrane/net_glue
|
import ../libs/membrane/net_glue
|
||||||
import ../libs/membrane/compositor
|
import ../libs/membrane/compositor
|
||||||
|
import ../libs/membrane/term
|
||||||
|
import pty
|
||||||
import sched
|
import sched
|
||||||
|
|
||||||
# Phase 31: Memory Manager imports (must be before usage)
|
# Phase 31: Memory Manager imports (must be before usage)
|
||||||
|
|
@ -531,11 +534,28 @@ proc k_handle_syscall*(nr, a0, a1, a2: uint): uint {.exportc, cdecl.} =
|
||||||
if a0 == 0: # STDIN Special Case
|
if a0 == 0: # STDIN Special Case
|
||||||
kprintln("[Kernel] sys_read(0) - STDIN request")
|
kprintln("[Kernel] sys_read(0) - STDIN request")
|
||||||
if (current_fiber.promises and PLEDGE_STDIO) == 0: return cast[uint](-1)
|
if (current_fiber.promises and PLEDGE_STDIO) == 0: return cast[uint](-1)
|
||||||
|
|
||||||
|
# Check if we have an active PTY for this fiber
|
||||||
|
# For now, use PTY 0 as the console PTY
|
||||||
|
if pty_has_data_for_slave(0):
|
||||||
|
var buf: array[1, byte]
|
||||||
|
let n = pty_read_slave(PTY_SLAVE_BASE, addr buf[0], 1)
|
||||||
|
if n > 0:
|
||||||
|
cast[ptr UncheckedArray[byte]](a1)[0] = buf[0]
|
||||||
|
return uint(n)
|
||||||
|
|
||||||
|
# Fallback to ION channel
|
||||||
var pkt: IonPacket
|
var pkt: IonPacket
|
||||||
if chan_input.recv(pkt):
|
if chan_input.recv(pkt):
|
||||||
kprintln("[Kernel] sys_read(0) - Data available")
|
kprintln("[Kernel] sys_read(0) - Data available")
|
||||||
let n = if uint64(pkt.len) < a2: uint64(pkt.len) else: a2
|
let n = if uint64(pkt.len) < a2: uint64(pkt.len) else: a2
|
||||||
if n > 0: copyMem(cast[pointer](a1), cast[pointer](pkt.data), int(n))
|
if n > 0: copyMem(cast[pointer](a1), cast[pointer](pkt.data), int(n))
|
||||||
|
|
||||||
|
# Also push to PTY for echo
|
||||||
|
let data = cast[ptr UncheckedArray[byte]](pkt.data)
|
||||||
|
for i in 0 ..< int(n):
|
||||||
|
pty_push_input(0, char(data[i]))
|
||||||
|
|
||||||
ion_free_raw(pkt.id)
|
ion_free_raw(pkt.id)
|
||||||
return n
|
return n
|
||||||
else:
|
else:
|
||||||
|
|
@ -558,6 +578,10 @@ proc k_handle_syscall*(nr, a0, a1, a2: uint): uint {.exportc, cdecl.} =
|
||||||
if a0 == 1 or a0 == 2: # STDOUT/STDERR
|
if a0 == 1 or a0 == 2: # STDOUT/STDERR
|
||||||
kprintln("[Kernel] sys_write(stdout) called")
|
kprintln("[Kernel] sys_write(stdout) called")
|
||||||
console_write(cast[pointer](a1), csize_t(a2))
|
console_write(cast[pointer](a1), csize_t(a2))
|
||||||
|
|
||||||
|
# Route through PTY slave (renders to FB terminal)
|
||||||
|
discard pty_write_slave(PTY_SLAVE_BASE, cast[ptr byte](a1), int(a2))
|
||||||
|
|
||||||
kprintln("[Kernel] sys_write(stdout) returning")
|
kprintln("[Kernel] sys_write(stdout) returning")
|
||||||
return a2
|
return a2
|
||||||
|
|
||||||
|
|
@ -652,6 +676,15 @@ proc kmain() {.exportc, cdecl.} =
|
||||||
|
|
||||||
ion_init_input()
|
ion_init_input()
|
||||||
hal_io_init()
|
hal_io_init()
|
||||||
|
|
||||||
|
# Phase 40: PTY Initialization (Soul Bridge)
|
||||||
|
pty_init()
|
||||||
|
discard pty_alloc() # Allocate PTY 0 as console PTY
|
||||||
|
kprintln("[PTY] Console PTY Allocated")
|
||||||
|
|
||||||
|
# Phase 39: FB Terminal Initialization (Sovereign Term)
|
||||||
|
term_init()
|
||||||
|
kprintln("[Term] Framebuffer Terminal Initialized")
|
||||||
|
|
||||||
vfs_init(addr binary_initrd_tar_start, addr binary_initrd_tar_end)
|
vfs_init(addr binary_initrd_tar_start, addr binary_initrd_tar_end)
|
||||||
vfs_mount_init()
|
vfs_mount_init()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,225 @@
|
||||||
|
# SPDX-License-Identifier: LSL-1.0
|
||||||
|
# Copyright (c) 2026 Markus Maiwald
|
||||||
|
# Stewardship: Self Sovereign Society Foundation
|
||||||
|
#
|
||||||
|
# This file is part of the Nexus Sovereign Core.
|
||||||
|
# See legal/LICENSE_SOVEREIGN.md for license terms.
|
||||||
|
|
||||||
|
## Nexus Core: Pseudo-Terminal (PTY) Subsystem
|
||||||
|
##
|
||||||
|
## Provides a POSIX-like PTY interface for terminal emulation.
|
||||||
|
## Master fd is held by terminal emulator, slave fd by shell.
|
||||||
|
##
|
||||||
|
## Phase 40: The Soul Bridge (PTY Implementation)
|
||||||
|
|
||||||
|
import ../libs/membrane/term
|
||||||
|
|
||||||
|
const
|
||||||
|
MAX_PTYS* = 8
|
||||||
|
PTY_BUFFER_SIZE* = 4096
|
||||||
|
|
||||||
|
# File descriptor ranges
|
||||||
|
PTY_MASTER_BASE* = 100 # Master fds: 100-107
|
||||||
|
PTY_SLAVE_BASE* = 200 # Slave fds: 200-207
|
||||||
|
|
||||||
|
type
|
||||||
|
LineMode* = enum
|
||||||
|
lmRaw, # No processing (binary mode)
|
||||||
|
lmCanon # Canonical mode (line buffering, echo)
|
||||||
|
|
||||||
|
PtyPair* = object
|
||||||
|
active*: bool
|
||||||
|
id*: int
|
||||||
|
|
||||||
|
# Buffers (bidirectional)
|
||||||
|
master_to_slave*: array[PTY_BUFFER_SIZE, byte]
|
||||||
|
mts_head*, mts_tail*: int
|
||||||
|
|
||||||
|
slave_to_master*: array[PTY_BUFFER_SIZE, byte]
|
||||||
|
stm_head*, stm_tail*: int
|
||||||
|
|
||||||
|
# Line discipline
|
||||||
|
mode*: LineMode
|
||||||
|
echo*: bool
|
||||||
|
|
||||||
|
# Window size
|
||||||
|
rows*, cols*: int
|
||||||
|
|
||||||
|
var ptys*: array[MAX_PTYS, PtyPair]
|
||||||
|
var next_pty_id: int = 0
|
||||||
|
|
||||||
|
# --- Logging ---
|
||||||
|
proc kprint(s: cstring) {.importc, cdecl.}
|
||||||
|
proc kprintln(s: cstring) {.importc, cdecl.}
|
||||||
|
proc kprint_hex(v: uint64) {.importc, cdecl.}
|
||||||
|
|
||||||
|
proc pty_init*() =
|
||||||
|
for i in 0 ..< MAX_PTYS:
|
||||||
|
ptys[i].active = false
|
||||||
|
ptys[i].id = -1
|
||||||
|
next_pty_id = 0
|
||||||
|
kprintln("[PTY] Subsystem Initialized")
|
||||||
|
|
||||||
|
proc pty_alloc*(): int =
|
||||||
|
## Allocate a new PTY pair. Returns PTY ID or -1 on failure.
|
||||||
|
for i in 0 ..< MAX_PTYS:
|
||||||
|
if not ptys[i].active:
|
||||||
|
ptys[i].active = true
|
||||||
|
ptys[i].id = next_pty_id
|
||||||
|
ptys[i].mts_head = 0
|
||||||
|
ptys[i].mts_tail = 0
|
||||||
|
ptys[i].stm_head = 0
|
||||||
|
ptys[i].stm_tail = 0
|
||||||
|
ptys[i].mode = lmCanon
|
||||||
|
ptys[i].echo = true
|
||||||
|
ptys[i].rows = 37
|
||||||
|
ptys[i].cols = 100
|
||||||
|
next_pty_id += 1
|
||||||
|
kprint("[PTY] Allocated ID=")
|
||||||
|
kprint_hex(uint64(ptys[i].id))
|
||||||
|
kprintln("")
|
||||||
|
return ptys[i].id
|
||||||
|
kprintln("[PTY] ERROR: Max PTYs allocated")
|
||||||
|
return -1
|
||||||
|
|
||||||
|
proc pty_get_master_fd*(pty_id: int): int =
|
||||||
|
## Get the master file descriptor for a PTY.
|
||||||
|
if pty_id < 0 or pty_id >= MAX_PTYS: return -1
|
||||||
|
if not ptys[pty_id].active: return -1
|
||||||
|
return PTY_MASTER_BASE + pty_id
|
||||||
|
|
||||||
|
proc pty_get_slave_fd*(pty_id: int): int =
|
||||||
|
## Get the slave file descriptor for a PTY.
|
||||||
|
if pty_id < 0 or pty_id >= MAX_PTYS: return -1
|
||||||
|
if not ptys[pty_id].active: return -1
|
||||||
|
return PTY_SLAVE_BASE + pty_id
|
||||||
|
|
||||||
|
proc is_pty_master_fd*(fd: int): bool =
|
||||||
|
return fd >= PTY_MASTER_BASE and fd < PTY_MASTER_BASE + MAX_PTYS
|
||||||
|
|
||||||
|
proc is_pty_slave_fd*(fd: int): bool =
|
||||||
|
return fd >= PTY_SLAVE_BASE and fd < PTY_SLAVE_BASE + MAX_PTYS
|
||||||
|
|
||||||
|
proc get_pty_from_fd*(fd: int): ptr PtyPair =
|
||||||
|
if is_pty_master_fd(fd):
|
||||||
|
let idx = fd - PTY_MASTER_BASE
|
||||||
|
if ptys[idx].active: return addr ptys[idx]
|
||||||
|
elif is_pty_slave_fd(fd):
|
||||||
|
let idx = fd - PTY_SLAVE_BASE
|
||||||
|
if ptys[idx].active: return addr ptys[idx]
|
||||||
|
return nil
|
||||||
|
|
||||||
|
# --- Buffer Operations ---
|
||||||
|
|
||||||
|
proc ring_push(buf: var array[PTY_BUFFER_SIZE, byte], head, tail: var int, data: byte): bool =
|
||||||
|
let next = (tail + 1) mod PTY_BUFFER_SIZE
|
||||||
|
if next == head: return false # Buffer full
|
||||||
|
buf[tail] = data
|
||||||
|
tail = next
|
||||||
|
return true
|
||||||
|
|
||||||
|
proc ring_pop(buf: var array[PTY_BUFFER_SIZE, byte], head, tail: var int): int =
|
||||||
|
if head == tail: return -1 # Buffer empty
|
||||||
|
let b = int(buf[head])
|
||||||
|
head = (head + 1) mod PTY_BUFFER_SIZE
|
||||||
|
return b
|
||||||
|
|
||||||
|
proc ring_count(head, tail: int): int =
|
||||||
|
if tail >= head:
|
||||||
|
return tail - head
|
||||||
|
else:
|
||||||
|
return PTY_BUFFER_SIZE - head + tail
|
||||||
|
|
||||||
|
# --- I/O Operations ---
|
||||||
|
|
||||||
|
proc pty_write_master*(fd: int, data: ptr byte, len: int): int =
|
||||||
|
## Write to master (goes to slave input). Called by terminal emulator.
|
||||||
|
let pty = get_pty_from_fd(fd)
|
||||||
|
if pty == nil: return -1
|
||||||
|
|
||||||
|
var written = 0
|
||||||
|
for i in 0 ..< len:
|
||||||
|
let b = cast[ptr UncheckedArray[byte]](data)[i]
|
||||||
|
if ring_push(pty.master_to_slave, pty.mts_head, pty.mts_tail, b):
|
||||||
|
written += 1
|
||||||
|
else:
|
||||||
|
break # Buffer full
|
||||||
|
return written
|
||||||
|
|
||||||
|
proc pty_read_master*(fd: int, data: ptr byte, len: int): int =
|
||||||
|
## Read from master (gets slave output). Called by terminal emulator.
|
||||||
|
let pty = get_pty_from_fd(fd)
|
||||||
|
if pty == nil: return -1
|
||||||
|
|
||||||
|
var read_count = 0
|
||||||
|
let buf = cast[ptr UncheckedArray[byte]](data)
|
||||||
|
for i in 0 ..< len:
|
||||||
|
let b = ring_pop(pty.slave_to_master, pty.stm_head, pty.stm_tail)
|
||||||
|
if b < 0: break
|
||||||
|
buf[i] = byte(b)
|
||||||
|
read_count += 1
|
||||||
|
return read_count
|
||||||
|
|
||||||
|
proc pty_write_slave*(fd: int, data: ptr byte, len: int): int =
|
||||||
|
## Write to slave (output from shell). Goes to master read buffer.
|
||||||
|
## Also renders to FB terminal.
|
||||||
|
let pty = get_pty_from_fd(fd)
|
||||||
|
if pty == nil: return -1
|
||||||
|
|
||||||
|
var written = 0
|
||||||
|
let buf = cast[ptr UncheckedArray[byte]](data)
|
||||||
|
for i in 0 ..< len:
|
||||||
|
let b = buf[i]
|
||||||
|
|
||||||
|
# Push to slave-to-master buffer (for terminal emulator)
|
||||||
|
if ring_push(pty.slave_to_master, pty.stm_head, pty.stm_tail, b):
|
||||||
|
written += 1
|
||||||
|
|
||||||
|
# Also render to FB terminal
|
||||||
|
term_putc(char(b))
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Render frame after batch write
|
||||||
|
if written > 0:
|
||||||
|
term_render()
|
||||||
|
|
||||||
|
return written
|
||||||
|
|
||||||
|
proc pty_read_slave*(fd: int, data: ptr byte, len: int): int =
|
||||||
|
## Read from slave (input to shell). Gets master input.
|
||||||
|
let pty = get_pty_from_fd(fd)
|
||||||
|
if pty == nil: return -1
|
||||||
|
|
||||||
|
var read_count = 0
|
||||||
|
let buf = cast[ptr UncheckedArray[byte]](data)
|
||||||
|
for i in 0 ..< len:
|
||||||
|
let b = ring_pop(pty.master_to_slave, pty.mts_head, pty.mts_tail)
|
||||||
|
if b < 0: break
|
||||||
|
buf[i] = byte(b)
|
||||||
|
read_count += 1
|
||||||
|
|
||||||
|
# Echo if enabled
|
||||||
|
if pty.echo and pty.mode == lmCanon:
|
||||||
|
discard ring_push(pty.slave_to_master, pty.stm_head, pty.stm_tail, byte(b))
|
||||||
|
term_putc(char(b))
|
||||||
|
|
||||||
|
if read_count > 0 and pty.echo:
|
||||||
|
term_render()
|
||||||
|
|
||||||
|
return read_count
|
||||||
|
|
||||||
|
proc pty_has_data_for_slave*(pty_id: int): bool =
|
||||||
|
## Check if there's input waiting for the slave.
|
||||||
|
if pty_id < 0 or pty_id >= MAX_PTYS: return false
|
||||||
|
if not ptys[pty_id].active: return false
|
||||||
|
return ring_count(ptys[pty_id].mts_head, ptys[pty_id].mts_tail) > 0
|
||||||
|
|
||||||
|
proc pty_push_input*(pty_id: int, ch: char) =
|
||||||
|
## Push a character to the master-to-slave buffer (keyboard input).
|
||||||
|
if pty_id < 0 or pty_id >= MAX_PTYS: return
|
||||||
|
if not ptys[pty_id].active: return
|
||||||
|
discard ring_push(ptys[pty_id].master_to_slave,
|
||||||
|
ptys[pty_id].mts_head,
|
||||||
|
ptys[pty_id].mts_tail,
|
||||||
|
byte(ch))
|
||||||
Loading…
Reference in New Issue