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/vfs
|
||||
import netswitch
|
||||
import ../libs/membrane/libc
|
||||
import ../libs/membrane/net_glue
|
||||
import ../libs/membrane/compositor
|
||||
import ../libs/membrane/term
|
||||
import pty
|
||||
import sched
|
||||
|
||||
# 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
|
||||
kprintln("[Kernel] sys_read(0) - STDIN request")
|
||||
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
|
||||
if chan_input.recv(pkt):
|
||||
kprintln("[Kernel] sys_read(0) - Data available")
|
||||
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))
|
||||
|
||||
# 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)
|
||||
return n
|
||||
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
|
||||
kprintln("[Kernel] sys_write(stdout) called")
|
||||
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")
|
||||
return a2
|
||||
|
||||
|
|
@ -652,6 +676,15 @@ proc kmain() {.exportc, cdecl.} =
|
|||
|
||||
ion_init_input()
|
||||
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_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