diff --git a/kernel/Makefile b/kernel/Makefile index 80fe92d9..0959487a 100644 --- a/kernel/Makefile +++ b/kernel/Makefile @@ -74,6 +74,7 @@ CXX_SOURCES += $(shell find fs -name '*.cpp' 2>/dev/null | sort) CXX_SOURCES += $(shell find resource -name '*.cpp' 2>/dev/null | sort) CXX_SOURCES += $(shell find socket -name '*.cpp' 2>/dev/null | sort) CXX_SOURCES += $(shell find terminal -name '*.cpp' 2>/dev/null | sort) +CXX_SOURCES += $(shell find pty -name '*.cpp' 2>/dev/null | sort) CXX_SOURCES += $(shell find arch/$(ARCH) -name '*.cpp' 2>/dev/null | sort) # Unit test sources (only when STLX_UNIT_TESTS_ENABLED=1) diff --git a/kernel/pty/pty.cpp b/kernel/pty/pty.cpp new file mode 100644 index 00000000..eeaff0eb --- /dev/null +++ b/kernel/pty/pty.cpp @@ -0,0 +1,228 @@ +#include "pty/pty.h" +#include "resource/resource.h" +#include "common/ring_buffer.h" +#include "fs/fstypes.h" +#include "mm/heap.h" +#include "dynpriv/dynpriv.h" + +namespace pty { + +__PRIVILEGED_BSS static uint32_t g_next_pty_id; + +__PRIVILEGED_CODE void pty_channel::ref_destroy(pty_channel* self) { + if (!self) { + return; + } + ring_buffer_destroy(self->m_input_rb); + ring_buffer_destroy(self->m_output_rb); + heap::kfree_delete(self); +} + +__PRIVILEGED_CODE static void pty_echo_fn(void* ctx, const uint8_t* buf, size_t len) { + auto* chan = static_cast(ctx); + (void)ring_buffer_write(chan->m_output_rb, buf, len, true); +} + +// Master ops + +static ssize_t pty_master_read( + resource::resource_object* obj, void* kdst, size_t count, uint32_t flags +) { + if (!obj || !obj->impl || !kdst) { + return resource::ERR_INVAL; + } + auto* ep = static_cast(obj->impl); + bool nonblock = (flags & fs::O_NONBLOCK) != 0; + ssize_t result; + RUN_ELEVATED({ + result = ring_buffer_read(ep->channel->m_output_rb, + static_cast(kdst), count, nonblock); + }); + return result; +} + +static ssize_t pty_master_write( + resource::resource_object* obj, const void* ksrc, size_t count, uint32_t flags +) { + (void)flags; + if (!obj || !obj->impl || !ksrc) { + return resource::ERR_INVAL; + } + auto* ep = static_cast(obj->impl); + auto* chan = ep->channel.ptr(); + + ssize_t result; + RUN_ELEVATED({ + if (chan->m_input_rb->reader_closed) { + result = resource::ERR_PIPE; + } else { + terminal::ld_input_buf(&chan->m_ld, chan->m_input_rb, &chan->m_echo, + static_cast(ksrc), count); + result = static_cast(count); + } + }); + return result; +} + +static void pty_master_close(resource::resource_object* obj) { + if (!obj || !obj->impl) { + return; + } + auto* ep = static_cast(obj->impl); + + RUN_ELEVATED({ + ring_buffer_close_write(ep->channel->m_input_rb); + ring_buffer_close_read(ep->channel->m_output_rb); + heap::kfree_delete(ep); + }); + obj->impl = nullptr; +} + +// Slave ops + +static ssize_t pty_slave_read( + resource::resource_object* obj, void* kdst, size_t count, uint32_t flags +) { + if (!obj || !obj->impl || !kdst) { + return resource::ERR_INVAL; + } + auto* ep = static_cast(obj->impl); + bool nonblock = (flags & fs::O_NONBLOCK) != 0; + ssize_t result; + RUN_ELEVATED({ + result = ring_buffer_read(ep->channel->m_input_rb, + static_cast(kdst), count, nonblock); + }); + return result; +} + +static ssize_t pty_slave_write( + resource::resource_object* obj, const void* ksrc, size_t count, uint32_t flags +) { + if (!obj || !obj->impl || !ksrc) { + return resource::ERR_INVAL; + } + auto* ep = static_cast(obj->impl); + bool nonblock = (flags & fs::O_NONBLOCK) != 0; + ssize_t result; + RUN_ELEVATED({ + result = ring_buffer_write(ep->channel->m_output_rb, + static_cast(ksrc), count, nonblock); + }); + return result; +} + +static void pty_slave_close(resource::resource_object* obj) { + if (!obj || !obj->impl) { + return; + } + auto* ep = static_cast(obj->impl); + + RUN_ELEVATED({ + ring_buffer_close_write(ep->channel->m_output_rb); + ring_buffer_close_read(ep->channel->m_input_rb); + heap::kfree_delete(ep); + }); + obj->impl = nullptr; +} + +static int32_t pty_slave_ioctl( + resource::resource_object* obj, uint32_t cmd, uint64_t arg +) { + (void)arg; + if (!obj || !obj->impl) { + return resource::ERR_INVAL; + } + auto* ep = static_cast(obj->impl); + return terminal::ld_set_mode(&ep->channel->m_ld, cmd); +} + +// Ops tables + +static const resource::resource_ops g_pty_master_ops = { + pty_master_read, + pty_master_write, + pty_master_close, + nullptr, +}; + +static const resource::resource_ops g_pty_slave_ops = { + pty_slave_read, + pty_slave_write, + pty_slave_close, + pty_slave_ioctl, +}; + +// Pair creation + +__PRIVILEGED_CODE int32_t create_pair( + resource::resource_object** out_master, + resource::resource_object** out_slave +) { + if (!out_master || !out_slave) { + return resource::ERR_INVAL; + } + + auto chan = rc::make_kref(); + if (!chan) { + return resource::ERR_NOMEM; + } + + chan->m_input_rb = ring_buffer_create(PTY_RING_CAPACITY); + if (!chan->m_input_rb) { + return resource::ERR_NOMEM; + } + + chan->m_output_rb = ring_buffer_create(PTY_RING_CAPACITY); + if (!chan->m_output_rb) { + ring_buffer_destroy(chan->m_input_rb); + chan->m_input_rb = nullptr; + return resource::ERR_NOMEM; + } + + terminal::ld_init(&chan->m_ld); + chan->m_echo = { pty_echo_fn, chan.ptr() }; + chan->m_id = __atomic_fetch_add(&g_next_pty_id, 1, __ATOMIC_RELAXED); + + auto* ep_master = heap::kalloc_new(); + if (!ep_master) { + return resource::ERR_NOMEM; + } + ep_master->channel = chan; + ep_master->is_master = true; + + auto* ep_slave = heap::kalloc_new(); + if (!ep_slave) { + heap::kfree_delete(ep_master); + return resource::ERR_NOMEM; + } + ep_slave->channel = static_cast&&>(chan); + ep_slave->is_master = false; + + auto* obj_master = heap::kalloc_new(); + if (!obj_master) { + heap::kfree_delete(ep_slave); + heap::kfree_delete(ep_master); + return resource::ERR_NOMEM; + } + obj_master->type = resource::resource_type::PTY; + obj_master->ops = &g_pty_master_ops; + obj_master->impl = ep_master; + + auto* obj_slave = heap::kalloc_new(); + if (!obj_slave) { + heap::kfree_delete(obj_master); + heap::kfree_delete(ep_slave); + heap::kfree_delete(ep_master); + return resource::ERR_NOMEM; + } + obj_slave->type = resource::resource_type::PTY; + obj_slave->ops = &g_pty_slave_ops; + obj_slave->impl = ep_slave; + + *out_master = obj_master; + *out_slave = obj_slave; + return resource::OK; +} + +} // namespace pty diff --git a/kernel/pty/pty.h b/kernel/pty/pty.h new file mode 100644 index 00000000..98cff13e --- /dev/null +++ b/kernel/pty/pty.h @@ -0,0 +1,46 @@ +#ifndef STELLUX_PTY_PTY_H +#define STELLUX_PTY_PTY_H + +#include "common/types.h" +#include "rc/ref_counted.h" +#include "rc/strong_ref.h" +#include "terminal/line_discipline.h" + +struct ring_buffer; +namespace resource { struct resource_object; } + +namespace pty { + +constexpr int32_t OK = 0; +constexpr int32_t ERR = -1; +constexpr size_t PTY_RING_CAPACITY = 4096; + +struct pty_channel : rc::ref_counted { + ring_buffer* m_input_rb; // master write -> ld -> slave read + ring_buffer* m_output_rb; // slave write -> master read + terminal::line_discipline m_ld; + terminal::echo_target m_echo; + uint32_t m_id; + + /** @note Privilege: **required** */ + __PRIVILEGED_CODE static void ref_destroy(pty_channel* self); +}; + +struct pty_endpoint { + rc::strong_ref channel; + bool is_master; +}; + +/** + * @brief Create a connected PTY master/slave pair. + * Returns two resource_objects, each with refcount 1. + * @note Privilege: **required** + */ +__PRIVILEGED_CODE int32_t create_pair( + resource::resource_object** out_master, + resource::resource_object** out_slave +); + +} // namespace pty + +#endif // STELLUX_PTY_PTY_H diff --git a/kernel/resource/handle_table.cpp b/kernel/resource/handle_table.cpp index 66c007d2..03fbd23f 100644 --- a/kernel/resource/handle_table.cpp +++ b/kernel/resource/handle_table.cpp @@ -72,7 +72,8 @@ __PRIVILEGED_CODE int32_t get_handle_object( handle_t handle, uint32_t required_rights, resource_object** out_obj, - uint32_t* out_flags + uint32_t* out_flags, + uint32_t* out_rights ) { if (!table || !out_obj) { return HANDLE_ERR_INVAL; @@ -98,6 +99,9 @@ __PRIVILEGED_CODE int32_t get_handle_object( if (out_flags) { *out_flags = entry.flags; } + if (out_rights) { + *out_rights = entry.rights; + } return HANDLE_OK; } @@ -151,6 +155,50 @@ __PRIVILEGED_CODE int32_t set_handle_flags( return HANDLE_OK; } +/** + * @note Privilege: **required** + */ +__PRIVILEGED_CODE int32_t install_handle_at( + handle_table* table, + handle_t slot, + resource_object* obj, + resource_type type, + uint32_t rights +) { + if (!table || !obj) { + return HANDLE_ERR_INVAL; + } + if (slot < 0 || static_cast(slot) >= MAX_TASK_HANDLES) { + return HANDLE_ERR_INVAL; + } + if (type == resource_type::UNKNOWN) { + return HANDLE_ERR_INVAL; + } + + resource_object* old_obj = nullptr; + { + sync::irq_lock_guard guard(table->lock); + handle_entry& entry = table->entries[static_cast(slot)]; + + if (entry.used && entry.obj) { + old_obj = entry.obj; + } + + resource_add_ref(obj); + entry.used = true; + entry.generation++; + entry.flags = 0; + entry.rights = rights; + entry.type = type; + entry.obj = obj; + } + + if (old_obj) { + resource_release(old_obj); + } + return HANDLE_OK; +} + /** * @note Privilege: **required** */ diff --git a/kernel/resource/handle_table.h b/kernel/resource/handle_table.h index c8de9cd8..62f0f43a 100644 --- a/kernel/resource/handle_table.h +++ b/kernel/resource/handle_table.h @@ -59,7 +59,8 @@ __PRIVILEGED_CODE int32_t get_handle_object( handle_t handle, uint32_t required_rights, resource_object** out_obj, - uint32_t* out_flags = nullptr + uint32_t* out_flags = nullptr, + uint32_t* out_rights = nullptr ); /** @@ -82,6 +83,20 @@ __PRIVILEGED_CODE int32_t set_handle_flags( uint32_t flags ); +/** + * @brief Install a resource at a specific slot, replacing any existing handle. + * If the slot is occupied, the old handle is removed and its object released. + * Increments object refcount on success. + * @note Privilege: **required** + */ +__PRIVILEGED_CODE int32_t install_handle_at( + handle_table* table, + handle_t slot, + resource_object* obj, + resource_type type, + uint32_t rights +); + /** * @brief Remove handle entry and return held object reference. * Does not release object; caller owns one reference on success. diff --git a/kernel/resource/resource_types.h b/kernel/resource/resource_types.h index a449fc71..ecb94c74 100644 --- a/kernel/resource/resource_types.h +++ b/kernel/resource/resource_types.h @@ -12,6 +12,7 @@ enum class resource_type : uint16_t { SHMEM = 3, PROCESS = 4, TERMINAL = 5, + PTY = 6, }; using handle_t = int32_t; diff --git a/kernel/syscall/handlers/sys_proc.cpp b/kernel/syscall/handlers/sys_proc.cpp index fa50b2de..c3f02cf7 100644 --- a/kernel/syscall/handlers/sys_proc.cpp +++ b/kernel/syscall/handlers/sys_proc.cpp @@ -336,3 +336,62 @@ DEFINE_SYSCALL2(proc_info, u_handle, u_info_ptr) { resource::resource_release(obj); return 0; } + +DEFINE_SYSCALL3(proc_set_handle, u_proc_handle, u_slot, u_resource_handle) { + sched::task* caller = sched::current(); + if (!caller) return syscall::EIO; + + int32_t slot = static_cast(u_slot); + if (slot < 0 || static_cast(slot) >= resource::MAX_TASK_HANDLES) { + return syscall::EINVAL; + } + + resource::resource_object* proc_obj = nullptr; + int32_t rc = resource::get_handle_object( + &caller->handles, static_cast(u_proc_handle), 0, &proc_obj); + if (rc != resource::HANDLE_OK) { + return syscall::EBADF; + } + + if (proc_obj->type != resource::resource_type::PROCESS) { + resource::resource_release(proc_obj); + return syscall::EBADF; + } + + auto* pr = resource::proc_provider::get_proc_resource(proc_obj); + if (!pr) { + resource::resource_release(proc_obj); + return syscall::EINVAL; + } + + sync::irq_state irq = sync::spin_lock_irqsave(pr->lock); + if (!pr->child || pr->child->state != sched::TASK_STATE_CREATED) { + sync::spin_unlock_irqrestore(pr->lock, irq); + resource::resource_release(proc_obj); + return syscall::EINVAL; + } + + resource::resource_object* res_obj = nullptr; + uint32_t res_rights = 0; + rc = resource::get_handle_object( + &caller->handles, static_cast(u_resource_handle), 0, + &res_obj, nullptr, &res_rights); + if (rc != resource::HANDLE_OK) { + sync::spin_unlock_irqrestore(pr->lock, irq); + resource::resource_release(proc_obj); + return syscall::EBADF; + } + + rc = resource::install_handle_at( + &pr->child->handles, static_cast(slot), + res_obj, res_obj->type, res_rights); + + sync::spin_unlock_irqrestore(pr->lock, irq); + resource::resource_release(res_obj); + resource::resource_release(proc_obj); + + if (rc != resource::HANDLE_OK) { + return syscall::EINVAL; + } + return 0; +} diff --git a/kernel/syscall/handlers/sys_proc.h b/kernel/syscall/handlers/sys_proc.h index bfa0540d..1544db9e 100644 --- a/kernel/syscall/handlers/sys_proc.h +++ b/kernel/syscall/handlers/sys_proc.h @@ -8,5 +8,6 @@ DECLARE_SYSCALL(proc_start); DECLARE_SYSCALL(proc_wait); DECLARE_SYSCALL(proc_detach); DECLARE_SYSCALL(proc_info); +DECLARE_SYSCALL(proc_set_handle); #endif // STELLUX_SYSCALL_HANDLERS_SYS_PROC_H diff --git a/kernel/syscall/handlers/sys_pty.cpp b/kernel/syscall/handlers/sys_pty.cpp new file mode 100644 index 00000000..a1207879 --- /dev/null +++ b/kernel/syscall/handlers/sys_pty.cpp @@ -0,0 +1,54 @@ +#include "syscall/handlers/sys_pty.h" +#include "syscall/syscall_table.h" +#include "resource/resource.h" +#include "pty/pty.h" +#include "sched/sched.h" +#include "sched/task.h" +#include "mm/uaccess.h" + +DEFINE_SYSCALL1(pty_create, u_fds) { + if (u_fds == 0) return syscall::EFAULT; + + sched::task* task = sched::current(); + if (!task) return syscall::EIO; + + resource::resource_object* master_obj = nullptr; + resource::resource_object* slave_obj = nullptr; + int32_t rc = pty::create_pair(&master_obj, &slave_obj); + if (rc != resource::OK) { + return syscall::ENOMEM; + } + + resource::handle_t h0 = -1; + rc = resource::alloc_handle( + &task->handles, master_obj, resource::resource_type::PTY, + resource::RIGHT_READ | resource::RIGHT_WRITE, &h0); + if (rc != resource::HANDLE_OK) { + resource::resource_release(master_obj); + resource::resource_release(slave_obj); + return syscall::EMFILE; + } + resource::resource_release(master_obj); + + resource::handle_t h1 = -1; + rc = resource::alloc_handle( + &task->handles, slave_obj, resource::resource_type::PTY, + resource::RIGHT_READ | resource::RIGHT_WRITE, &h1); + if (rc != resource::HANDLE_OK) { + resource::close(task, h0); + resource::resource_release(slave_obj); + return syscall::EMFILE; + } + resource::resource_release(slave_obj); + + int32_t kbuf[2] = {h0, h1}; + int32_t copy_rc = mm::uaccess::copy_to_user( + reinterpret_cast(u_fds), kbuf, sizeof(kbuf)); + if (copy_rc != mm::uaccess::OK) { + resource::close(task, h1); + resource::close(task, h0); + return syscall::EFAULT; + } + + return 0; +} diff --git a/kernel/syscall/handlers/sys_pty.h b/kernel/syscall/handlers/sys_pty.h new file mode 100644 index 00000000..606e29b1 --- /dev/null +++ b/kernel/syscall/handlers/sys_pty.h @@ -0,0 +1,8 @@ +#ifndef STELLUX_SYSCALL_HANDLERS_SYS_PTY_H +#define STELLUX_SYSCALL_HANDLERS_SYS_PTY_H + +#include "syscall/syscall_table.h" + +DECLARE_SYSCALL(pty_create); + +#endif // STELLUX_SYSCALL_HANDLERS_SYS_PTY_H diff --git a/kernel/syscall/syscall.h b/kernel/syscall/syscall.h index ea02b312..b56b7d09 100644 --- a/kernel/syscall/syscall.h +++ b/kernel/syscall/syscall.h @@ -17,7 +17,11 @@ constexpr uint64_t SYS_PROC_CREATE = 1010; constexpr uint64_t SYS_PROC_START = 1011; constexpr uint64_t SYS_PROC_WAIT = 1012; constexpr uint64_t SYS_PROC_DETACH = 1013; -constexpr uint64_t SYS_PROC_INFO = 1014; +constexpr uint64_t SYS_PROC_INFO = 1014; +constexpr uint64_t SYS_PROC_SET_HANDLE = 1015; + +// PTY +constexpr uint64_t SYS_PTY_CREATE = 1020; /** * Architecture-specific syscall initialization (MSRs on x86, etc.) diff --git a/kernel/syscall/syscall_table.cpp b/kernel/syscall/syscall_table.cpp index 1196a586..7f70ec1c 100644 --- a/kernel/syscall/syscall_table.cpp +++ b/kernel/syscall/syscall_table.cpp @@ -9,6 +9,7 @@ #include "syscall/handlers/sys_socket.h" #include "syscall/handlers/sys_memfd.h" #include "syscall/handlers/sys_proc.h" +#include "syscall/handlers/sys_pty.h" namespace syscall { @@ -65,6 +66,9 @@ __PRIVILEGED_CODE void init_syscall_table() { REGISTER_SYSCALL(SYS_PROC_WAIT, proc_wait); REGISTER_SYSCALL(SYS_PROC_DETACH, proc_detach); REGISTER_SYSCALL(SYS_PROC_INFO, proc_info); + REGISTER_SYSCALL(SYS_PROC_SET_HANDLE, proc_set_handle); + + REGISTER_SYSCALL(SYS_PTY_CREATE, pty_create); register_arch_syscalls(); } diff --git a/kernel/terminal/line_discipline.cpp b/kernel/terminal/line_discipline.cpp new file mode 100644 index 00000000..12913be4 --- /dev/null +++ b/kernel/terminal/line_discipline.cpp @@ -0,0 +1,117 @@ +#include "terminal/line_discipline.h" +#include "terminal/terminal.h" +#include "common/ring_buffer.h" +#include "dynpriv/dynpriv.h" + +namespace terminal { + +void ld_init(line_discipline* ld) { + ld->mode = 0; + ld->line_len = 0; + ld->prev_char = 0; + ld->lock = sync::SPINLOCK_INIT; +} + +static void ld_process_byte(line_discipline* ld, ring_buffer* sink, + const echo_target* echo, char c, + bool hold_lock, sync::irq_state& irq) { + if (ld->mode == LD_MODE_RAW) { + ld->prev_char = c; + if (!hold_lock) sync::spin_unlock_irqrestore(ld->lock, irq); + uint8_t byte = static_cast(c); + (void)ring_buffer_write(sink, &byte, 1, true); + return; + } + + if (ld->prev_char == '\r' && c == '\n') { + ld->prev_char = c; + if (!hold_lock) sync::spin_unlock_irqrestore(ld->lock, irq); + return; + } + ld->prev_char = c; + + if (c == '\r' || c == '\n') { + ld->line_buf[ld->line_len] = '\n'; + size_t len = ld->line_len + 1; + if (!hold_lock) sync::spin_unlock_irqrestore(ld->lock, irq); + (void)ring_buffer_write(sink, + reinterpret_cast(ld->line_buf), + len, true); + ld->line_len = 0; + if (echo && echo->write) { + static const uint8_t crlf[] = {'\r', '\n'}; + echo->write(echo->ctx, crlf, 2); + } + } else if (c == 0x7F || c == 0x08) { + if (ld->line_len > 0) { + ld->line_len--; + if (!hold_lock) sync::spin_unlock_irqrestore(ld->lock, irq); + if (echo && echo->write) { + static const uint8_t bs_seq[] = {'\b', ' ', '\b'}; + echo->write(echo->ctx, bs_seq, 3); + } + } else { + if (!hold_lock) sync::spin_unlock_irqrestore(ld->lock, irq); + } + } else if (c >= 0x20 && c <= 0x7E) { + if (ld->line_len < LD_LINE_BUF_MAX) { + ld->line_buf[ld->line_len++] = c; + if (!hold_lock) sync::spin_unlock_irqrestore(ld->lock, irq); + if (echo && echo->write) { + uint8_t byte = static_cast(c); + echo->write(echo->ctx, &byte, 1); + } + } else { + if (!hold_lock) sync::spin_unlock_irqrestore(ld->lock, irq); + } + } else { + if (!hold_lock) sync::spin_unlock_irqrestore(ld->lock, irq); + } +} + +__PRIVILEGED_CODE void ld_input(line_discipline* ld, ring_buffer* sink, + const echo_target* echo, char c) { + sync::irq_state irq = sync::spin_lock_irqsave(ld->lock); + ld_process_byte(ld, sink, echo, c, false, irq); +} + +__PRIVILEGED_CODE void ld_input_buf(line_discipline* ld, ring_buffer* sink, + const echo_target* echo, + const char* buf, size_t len) { + sync::irq_state irq = sync::spin_lock_irqsave(ld->lock); + + if (ld->mode == LD_MODE_RAW) { + sync::spin_unlock_irqrestore(ld->lock, irq); + (void)ring_buffer_write(sink, reinterpret_cast(buf), len, true); + return; + } + + for (size_t i = 0; i < len; i++) { + ld_process_byte(ld, sink, echo, buf[i], true, irq); + } + + sync::spin_unlock_irqrestore(ld->lock, irq); +} + +int32_t ld_set_mode(line_discipline* ld, uint32_t cmd) { + uint32_t new_mode; + if (cmd == STLX_TCSETS_RAW) { + new_mode = LD_MODE_RAW; + } else if (cmd == STLX_TCSETS_COOKED) { + new_mode = 0; + } else { + return ERR; + } + + RUN_ELEVATED({ + sync::irq_state irq = sync::spin_lock_irqsave(ld->lock); + if (ld->mode != new_mode) { + ld->line_len = 0; + ld->mode = new_mode; + } + sync::spin_unlock_irqrestore(ld->lock, irq); + }); + return OK; +} + +} // namespace terminal diff --git a/kernel/terminal/line_discipline.h b/kernel/terminal/line_discipline.h new file mode 100644 index 00000000..fe31e743 --- /dev/null +++ b/kernel/terminal/line_discipline.h @@ -0,0 +1,66 @@ +#ifndef STELLUX_TERMINAL_LINE_DISCIPLINE_H +#define STELLUX_TERMINAL_LINE_DISCIPLINE_H + +#include "common/types.h" +#include "sync/spinlock.h" + +struct ring_buffer; + +namespace terminal { + +constexpr size_t LD_LINE_BUF_MAX = 1023; +constexpr uint32_t LD_MODE_RAW = 1; + +struct echo_target { + void (*write)(void* ctx, const uint8_t* buf, size_t len); + void* ctx; +}; + +struct line_discipline { + uint32_t mode; + char line_buf[LD_LINE_BUF_MAX + 1]; + size_t line_len; + char prev_char; + sync::spinlock lock; +}; + +/** + * @brief Initialize a line discipline to cooked mode with empty state. + */ +void ld_init(line_discipline* ld); + +/** + * @brief Process a single input byte through the line discipline. + * Acquires and releases ld->lock per call. Safe from ISR context. + * @param ld Line discipline state. + * @param sink Ring buffer where processed input is delivered. + * @param echo Where echo bytes are sent (cooked mode only). + * @param c The input byte. + * @note Privilege: **required** + */ +__PRIVILEGED_CODE void ld_input(line_discipline* ld, ring_buffer* sink, + const echo_target* echo, char c); + +/** + * @brief Process a buffer of input bytes through the line discipline. + * Acquires ld->lock once for the entire buffer. More efficient than + * per-byte ld_input for process-context callers (PTY master write). + * Echo and ring_buffer_write are called while holding ld->lock + * and must be nonblocking. + * @note Privilege: **required** + */ +__PRIVILEGED_CODE void ld_input_buf(line_discipline* ld, ring_buffer* sink, + const echo_target* echo, + const char* buf, size_t len); + +/** + * @brief Switch between raw and cooked mode. Resets line buffer. + * Elevates internally for the spinlock critical section. + * @param cmd STLX_TCSETS_RAW or STLX_TCSETS_COOKED. + * @return OK on success, ERR on invalid cmd. + */ +int32_t ld_set_mode(line_discipline* ld, uint32_t cmd); + +} // namespace terminal + +#endif // STELLUX_TERMINAL_LINE_DISCIPLINE_H diff --git a/kernel/terminal/terminal.cpp b/kernel/terminal/terminal.cpp index 16f28387..3720367f 100644 --- a/kernel/terminal/terminal.cpp +++ b/kernel/terminal/terminal.cpp @@ -1,5 +1,6 @@ #include "terminal/terminal.h" #include "terminal/console_node.h" +#include "terminal/line_discipline.h" #include "common/ring_buffer.h" #include "io/serial.h" #include "resource/resource.h" @@ -7,23 +8,26 @@ #include "fs/fstypes.h" #include "fs/devfs/devfs.h" #include "mm/heap.h" -#include "sync/spinlock.h" namespace terminal { -constexpr uint32_t MODE_RAW = 1; -constexpr size_t LINE_BUF_MAX = 1023; // reserve 1 byte for \n constexpr size_t INPUT_RING_CAPACITY = 4096; __PRIVILEGED_BSS static struct { ring_buffer* input_rb; - uint32_t mode; - char line_buf[LINE_BUF_MAX + 1]; - size_t line_len; - char prev_char; - sync::spinlock lock; + line_discipline ld; } g_console; +__PRIVILEGED_CODE static void serial_echo(void* ctx, const uint8_t* buf, size_t len) { + (void)ctx; + serial::write(reinterpret_cast(buf), len); +} + +__PRIVILEGED_DATA static const echo_target g_serial_echo = { + serial_echo, + nullptr, +}; + __PRIVILEGED_CODE int32_t init() { g_console.input_rb = ring_buffer_create(INPUT_RING_CAPACITY); if (!g_console.input_rb) { @@ -31,6 +35,8 @@ __PRIVILEGED_CODE int32_t init() { return ERR; } + ld_init(&g_console.ld); + serial::set_rx_callback(input_char); if (serial::enable_rx_interrupt() != serial::OK) { log::warn("terminal: serial RX interrupt setup failed"); @@ -53,54 +59,7 @@ __PRIVILEGED_CODE int32_t init() { } __PRIVILEGED_CODE void input_char(char c) { - sync::irq_state irq = sync::spin_lock_irqsave(g_console.lock); - - if (g_console.prev_char == '\r' && c == '\n') { - g_console.prev_char = c; - sync::spin_unlock_irqrestore(g_console.lock, irq); - return; - } - g_console.prev_char = c; - - if (g_console.mode == MODE_RAW) { - sync::spin_unlock_irqrestore(g_console.lock, irq); - uint8_t byte = static_cast(c); - (void)ring_buffer_write(g_console.input_rb, &byte, 1, true); - return; - } - - // Cooked mode - if (c == '\r' || c == '\n') { - g_console.line_buf[g_console.line_len] = '\n'; - size_t len = g_console.line_len + 1; - sync::spin_unlock_irqrestore(g_console.lock, irq); - (void)ring_buffer_write(g_console.input_rb, - reinterpret_cast(g_console.line_buf), - len, true); - g_console.line_len = 0; - serial::write_char('\r'); - serial::write_char('\n'); - } else if (c == 0x7F || c == 0x08) { - if (g_console.line_len > 0) { - g_console.line_len--; - sync::spin_unlock_irqrestore(g_console.lock, irq); - serial::write_char('\b'); - serial::write_char(' '); - serial::write_char('\b'); - } else { - sync::spin_unlock_irqrestore(g_console.lock, irq); - } - } else if (c >= 0x20 && c <= 0x7E) { - if (g_console.line_len < LINE_BUF_MAX) { - g_console.line_buf[g_console.line_len++] = c; - sync::spin_unlock_irqrestore(g_console.lock, irq); - serial::write_char(c); - } else { - sync::spin_unlock_irqrestore(g_console.lock, irq); - } - } else { - sync::spin_unlock_irqrestore(g_console.lock, irq); - } + ld_input(&g_console.ld, g_console.input_rb, &g_serial_echo, c); } __PRIVILEGED_CODE ring_buffer* console_input_rb() { @@ -126,7 +85,6 @@ __PRIVILEGED_CODE static ssize_t terminal_write( } __PRIVILEGED_CODE static void terminal_close(resource::resource_object*) { - // Console is a singleton -- never destroyed. No-op. } static const resource::resource_ops g_terminal_ops = { @@ -136,27 +94,12 @@ static const resource::resource_ops g_terminal_ops = { nullptr, }; -__PRIVILEGED_CODE const resource::resource_ops* get_terminal_ops() { +const resource::resource_ops* get_terminal_ops() { return &g_terminal_ops; } -__PRIVILEGED_CODE int32_t set_mode(uint32_t cmd) { - uint32_t new_mode; - if (cmd == STLX_TCSETS_RAW) { - new_mode = MODE_RAW; - } else if (cmd == STLX_TCSETS_COOKED) { - new_mode = 0; - } else { - return ERR; - } - - sync::irq_state irq = sync::spin_lock_irqsave(g_console.lock); - if (g_console.mode != new_mode) { - g_console.line_len = 0; - g_console.mode = new_mode; - } - sync::spin_unlock_irqrestore(g_console.lock, irq); - return OK; +int32_t set_mode(uint32_t cmd) { + return ld_set_mode(&g_console.ld, cmd); } } // namespace terminal diff --git a/kernel/terminal/terminal.h b/kernel/terminal/terminal.h index 0b2f83e3..d8b0f6c5 100644 --- a/kernel/terminal/terminal.h +++ b/kernel/terminal/terminal.h @@ -39,18 +39,16 @@ __PRIVILEGED_CODE ring_buffer* console_input_rb(); /** * @brief Switch the console terminal between raw and cooked mode. - * Synchronized with the serial RX ISR via spinlock. + * Elevates internally for the spinlock critical section. * @param cmd STLX_TCSETS_RAW or STLX_TCSETS_COOKED. * @return OK on success, ERR on invalid cmd. - * @note Privilege: **required** */ -__PRIVILEGED_CODE int32_t set_mode(uint32_t cmd); +int32_t set_mode(uint32_t cmd); /** * @brief Get the terminal resource ops table for creating resource_objects. - * @note Privilege: **required** */ -__PRIVILEGED_CODE const resource::resource_ops* get_terminal_ops(); +const resource::resource_ops* get_terminal_ops(); } // namespace terminal diff --git a/kernel/tests/pty/pty.test.cpp b/kernel/tests/pty/pty.test.cpp new file mode 100644 index 00000000..98c1905d --- /dev/null +++ b/kernel/tests/pty/pty.test.cpp @@ -0,0 +1,204 @@ +#define STLX_TEST_TIER TIER_SCHED + +#include "stlx_unit_test.h" +#include "pty/pty.h" +#include "resource/resource.h" +#include "sched/sched.h" +#include "sched/task.h" +#include "terminal/terminal.h" +#include "terminal/line_discipline.h" +#include "common/ring_buffer.h" +#include "fs/fstypes.h" + +TEST_SUITE(pty_test); + +TEST(pty_test, create_pair_succeeds) { + resource::resource_object* master = nullptr; + resource::resource_object* slave = nullptr; + ASSERT_EQ(pty::create_pair(&master, &slave), resource::OK); + ASSERT_NOT_NULL(master); + ASSERT_NOT_NULL(slave); + EXPECT_EQ(master->type, resource::resource_type::PTY); + EXPECT_EQ(slave->type, resource::resource_type::PTY); + + resource::resource_release(master); + resource::resource_release(slave); +} + +TEST(pty_test, master_to_slave_write_read) { + sched::task* task = sched::current(); + ASSERT_NOT_NULL(task); + + resource::resource_object* master = nullptr; + resource::resource_object* slave = nullptr; + ASSERT_EQ(pty::create_pair(&master, &slave), resource::OK); + + resource::handle_t hm = -1; + resource::handle_t hs = -1; + ASSERT_EQ(resource::alloc_handle(&task->handles, master, resource::resource_type::PTY, + resource::RIGHT_READ | resource::RIGHT_WRITE, &hm), resource::HANDLE_OK); + resource::resource_release(master); + ASSERT_EQ(resource::alloc_handle(&task->handles, slave, resource::resource_type::PTY, + resource::RIGHT_READ | resource::RIGHT_WRITE, &hs), resource::HANDLE_OK); + resource::resource_release(slave); + + // Set raw mode so bytes pass through directly + auto* ep = static_cast(slave->impl); + terminal::ld_set_mode(&ep->channel->m_ld, terminal::STLX_TCSETS_RAW); + + const char msg[] = "hello"; + ASSERT_EQ(resource::write(task, hm, msg, 5), static_cast(5)); + + char buf[16] = {}; + ASSERT_EQ(resource::read(task, hs, buf, 16), static_cast(5)); + EXPECT_STREQ(buf, "hello"); + + EXPECT_EQ(resource::close(task, hm), resource::OK); + EXPECT_EQ(resource::close(task, hs), resource::OK); +} + +TEST(pty_test, slave_to_master_write_read) { + sched::task* task = sched::current(); + ASSERT_NOT_NULL(task); + + resource::resource_object* master = nullptr; + resource::resource_object* slave = nullptr; + ASSERT_EQ(pty::create_pair(&master, &slave), resource::OK); + + resource::handle_t hm = -1; + resource::handle_t hs = -1; + ASSERT_EQ(resource::alloc_handle(&task->handles, master, resource::resource_type::PTY, + resource::RIGHT_READ | resource::RIGHT_WRITE, &hm), resource::HANDLE_OK); + resource::resource_release(master); + ASSERT_EQ(resource::alloc_handle(&task->handles, slave, resource::resource_type::PTY, + resource::RIGHT_READ | resource::RIGHT_WRITE, &hs), resource::HANDLE_OK); + resource::resource_release(slave); + + const char msg[] = "world"; + ASSERT_EQ(resource::write(task, hs, msg, 5), static_cast(5)); + + char buf[16] = {}; + ASSERT_EQ(resource::read(task, hm, buf, 16), static_cast(5)); + EXPECT_STREQ(buf, "world"); + + EXPECT_EQ(resource::close(task, hm), resource::OK); + EXPECT_EQ(resource::close(task, hs), resource::OK); +} + +TEST(pty_test, close_master_slave_reads_eof) { + sched::task* task = sched::current(); + ASSERT_NOT_NULL(task); + + resource::resource_object* master = nullptr; + resource::resource_object* slave = nullptr; + ASSERT_EQ(pty::create_pair(&master, &slave), resource::OK); + + resource::handle_t hm = -1; + resource::handle_t hs = -1; + ASSERT_EQ(resource::alloc_handle(&task->handles, master, resource::resource_type::PTY, + resource::RIGHT_READ | resource::RIGHT_WRITE, &hm), resource::HANDLE_OK); + resource::resource_release(master); + ASSERT_EQ(resource::alloc_handle(&task->handles, slave, resource::resource_type::PTY, + resource::RIGHT_READ | resource::RIGHT_WRITE, &hs), resource::HANDLE_OK); + resource::resource_release(slave); + + EXPECT_EQ(resource::close(task, hm), resource::OK); + + char buf[16] = {}; + EXPECT_EQ(resource::read(task, hs, buf, 16), static_cast(0)); + + EXPECT_EQ(resource::close(task, hs), resource::OK); +} + +TEST(pty_test, close_slave_master_reads_eof) { + sched::task* task = sched::current(); + ASSERT_NOT_NULL(task); + + resource::resource_object* master = nullptr; + resource::resource_object* slave = nullptr; + ASSERT_EQ(pty::create_pair(&master, &slave), resource::OK); + + resource::handle_t hm = -1; + resource::handle_t hs = -1; + ASSERT_EQ(resource::alloc_handle(&task->handles, master, resource::resource_type::PTY, + resource::RIGHT_READ | resource::RIGHT_WRITE, &hm), resource::HANDLE_OK); + resource::resource_release(master); + ASSERT_EQ(resource::alloc_handle(&task->handles, slave, resource::resource_type::PTY, + resource::RIGHT_READ | resource::RIGHT_WRITE, &hs), resource::HANDLE_OK); + resource::resource_release(slave); + + EXPECT_EQ(resource::close(task, hs), resource::OK); + + char buf[16] = {}; + EXPECT_EQ(resource::read(task, hm, buf, 16), static_cast(0)); + + EXPECT_EQ(resource::close(task, hm), resource::OK); +} + +TEST(pty_test, close_slave_master_write_epipe) { + sched::task* task = sched::current(); + ASSERT_NOT_NULL(task); + + resource::resource_object* master = nullptr; + resource::resource_object* slave = nullptr; + ASSERT_EQ(pty::create_pair(&master, &slave), resource::OK); + + resource::handle_t hm = -1; + resource::handle_t hs = -1; + ASSERT_EQ(resource::alloc_handle(&task->handles, master, resource::resource_type::PTY, + resource::RIGHT_READ | resource::RIGHT_WRITE, &hm), resource::HANDLE_OK); + resource::resource_release(master); + ASSERT_EQ(resource::alloc_handle(&task->handles, slave, resource::resource_type::PTY, + resource::RIGHT_READ | resource::RIGHT_WRITE, &hs), resource::HANDLE_OK); + resource::resource_release(slave); + + // Set raw mode for simpler write semantics + auto* ep = static_cast(slave->impl); + terminal::ld_set_mode(&ep->channel->m_ld, terminal::STLX_TCSETS_RAW); + + EXPECT_EQ(resource::close(task, hs), resource::OK); + + EXPECT_EQ(resource::write(task, hm, "x", 1), static_cast(resource::ERR_PIPE)); + + EXPECT_EQ(resource::close(task, hm), resource::OK); +} + +TEST(pty_test, raw_mode_no_echo) { + sched::task* task = sched::current(); + ASSERT_NOT_NULL(task); + + resource::resource_object* master = nullptr; + resource::resource_object* slave = nullptr; + ASSERT_EQ(pty::create_pair(&master, &slave), resource::OK); + + resource::handle_t hm = -1; + resource::handle_t hs = -1; + ASSERT_EQ(resource::alloc_handle(&task->handles, master, resource::resource_type::PTY, + resource::RIGHT_READ | resource::RIGHT_WRITE, &hm), resource::HANDLE_OK); + resource::resource_release(master); + ASSERT_EQ(resource::alloc_handle(&task->handles, slave, resource::resource_type::PTY, + resource::RIGHT_READ | resource::RIGHT_WRITE, &hs), resource::HANDLE_OK); + resource::resource_release(slave); + + auto* ep = static_cast(slave->impl); + terminal::ld_set_mode(&ep->channel->m_ld, terminal::STLX_TCSETS_RAW); + + // Write to master (raw mode: no echo, bytes pass to slave input) + ASSERT_EQ(resource::write(task, hm, "abc", 3), static_cast(3)); + + // Set master to non-blocking so we can check for no echo without hanging + ASSERT_EQ(resource::set_handle_flags(&task->handles, hm, fs::O_NONBLOCK), resource::HANDLE_OK); + + // Master read should have nothing (no echo in raw mode) + char echo_buf[16] = {}; + ssize_t echo_rc = resource::read(task, hm, echo_buf, 16); + EXPECT_EQ(echo_rc, static_cast(resource::ERR_AGAIN)); + + // Slave read should have the bytes + char buf[16] = {}; + ASSERT_EQ(resource::read(task, hs, buf, 16), static_cast(3)); + EXPECT_STREQ(buf, "abc"); + + EXPECT_EQ(resource::close(task, hm), resource::OK); + EXPECT_EQ(resource::close(task, hs), resource::OK); +} diff --git a/userland/apps/Makefile b/userland/apps/Makefile index fa29ea17..29eeac2a 100644 --- a/userland/apps/Makefile +++ b/userland/apps/Makefile @@ -2,7 +2,7 @@ # Stellux Userland - Applications # -APP_DIRS := init hello shell ls cat rm stat touch sleep true false clear +APP_DIRS := init hello shell ls cat rm stat touch sleep true false clear ptytest APP_COUNT := $(words $(APP_DIRS)) all: diff --git a/userland/apps/ptytest/Makefile b/userland/apps/ptytest/Makefile new file mode 100644 index 00000000..10114d7c --- /dev/null +++ b/userland/apps/ptytest/Makefile @@ -0,0 +1,2 @@ +APP_NAME := ptytest +include ../../mk/app.mk diff --git a/userland/apps/ptytest/src/ptytest.c b/userland/apps/ptytest/src/ptytest.c new file mode 100644 index 00000000..64e2604b --- /dev/null +++ b/userland/apps/ptytest/src/ptytest.c @@ -0,0 +1,59 @@ +#include +#include +#include +#include +#include +#include + +#define STLX_TCSETS_RAW 0x5401 + +int main(void) { + setvbuf(stdout, NULL, _IONBF, 0); + + int master_fd, slave_fd; + if (pty_create(&master_fd, &slave_fd) < 0) { + printf("ptytest: pty_create failed\r\n"); + return 1; + } + printf("ptytest: created PTY pair (master=%d, slave=%d)\r\n", master_fd, slave_fd); + + ioctl(slave_fd, STLX_TCSETS_RAW, 0); + + const char* msg = "hello from master"; + ssize_t w = write(master_fd, msg, strlen(msg)); + printf("ptytest: wrote %ld bytes to master\r\n", (long)w); + + char buf[64] = {}; + ssize_t r = read(slave_fd, buf, sizeof(buf) - 1); + printf("ptytest: read %ld bytes from slave: \"%s\"\r\n", (long)r, buf); + + const char* reply = "hello from slave"; + w = write(slave_fd, reply, strlen(reply)); + printf("ptytest: wrote %ld bytes to slave\r\n", (long)w); + + memset(buf, 0, sizeof(buf)); + r = read(master_fd, buf, sizeof(buf) - 1); + printf("ptytest: read %ld bytes from master: \"%s\"\r\n", (long)r, buf); + + // Test proc_set_handle: launch hello with PTY slave as stdio + int proc = proc_create("/initrd/bin/hello", NULL); + if (proc >= 0) { + proc_set_handle(proc, 0, slave_fd); + proc_set_handle(proc, 1, slave_fd); + proc_set_handle(proc, 2, slave_fd); + proc_start(proc); + + memset(buf, 0, sizeof(buf)); + r = read(master_fd, buf, sizeof(buf) - 1); + printf("ptytest: child output via PTY: \"%s\"\r\n", buf); + + int exit_code = -1; + proc_wait(proc, &exit_code); + printf("ptytest: child exited with code %d\r\n", exit_code); + } + + close(slave_fd); + close(master_fd); + printf("ptytest: all tests passed\r\n"); + return 0; +} diff --git a/userland/lib/libstlx/include/stlx/proc.h b/userland/lib/libstlx/include/stlx/proc.h index 52f9ddd4..9f6fd89e 100644 --- a/userland/lib/libstlx/include/stlx/proc.h +++ b/userland/lib/libstlx/include/stlx/proc.h @@ -49,4 +49,11 @@ int proc_detach(int handle); */ int proc_info(int handle, process_info* info); +/** + * Install a resource handle at a specific fd slot in a child process. + * The child must be in CREATED state (not yet started). Replaces any + * existing handle at that slot. Returns 0 on success, -1 on failure. + */ +int proc_set_handle(int proc_handle, int slot, int resource_handle); + #endif /* STLX_PROC_H */ diff --git a/userland/lib/libstlx/include/stlx/pty.h b/userland/lib/libstlx/include/stlx/pty.h new file mode 100644 index 00000000..e6a749a2 --- /dev/null +++ b/userland/lib/libstlx/include/stlx/pty.h @@ -0,0 +1,11 @@ +#ifndef STLX_PTY_H +#define STLX_PTY_H + +/** + * Create a PTY master/slave pair. On success, *master_fd and *slave_fd + * are set to the new file descriptors. Returns 0 on success, -1 on + * failure with errno set. + */ +int pty_create(int* master_fd, int* slave_fd); + +#endif /* STLX_PTY_H */ diff --git a/userland/lib/libstlx/include/stlx/syscall_nums.h b/userland/lib/libstlx/include/stlx/syscall_nums.h index 8522a219..89f0e72b 100644 --- a/userland/lib/libstlx/include/stlx/syscall_nums.h +++ b/userland/lib/libstlx/include/stlx/syscall_nums.h @@ -5,6 +5,8 @@ #define SYS_PROC_START 1011 #define SYS_PROC_WAIT 1012 #define SYS_PROC_DETACH 1013 -#define SYS_PROC_INFO 1014 +#define SYS_PROC_INFO 1014 +#define SYS_PROC_SET_HANDLE 1015 +#define SYS_PTY_CREATE 1020 #endif /* STLX_SYSCALL_NUMS_H */ diff --git a/userland/lib/libstlx/src/proc.c b/userland/lib/libstlx/src/proc.c index a2e98287..32c4d183 100644 --- a/userland/lib/libstlx/src/proc.c +++ b/userland/lib/libstlx/src/proc.c @@ -35,3 +35,7 @@ int proc_detach(int handle) { int proc_info(int handle, process_info* info) { return (int)syscall(SYS_PROC_INFO, handle, info); } + +int proc_set_handle(int proc_handle, int slot, int resource_handle) { + return (int)syscall(SYS_PROC_SET_HANDLE, proc_handle, slot, resource_handle); +} diff --git a/userland/lib/libstlx/src/pty.c b/userland/lib/libstlx/src/pty.c new file mode 100644 index 00000000..5661556e --- /dev/null +++ b/userland/lib/libstlx/src/pty.c @@ -0,0 +1,13 @@ +#define _GNU_SOURCE +#include +#include +#include + +int pty_create(int* master_fd, int* slave_fd) { + int fds[2]; + int rc = (int)syscall(SYS_PTY_CREATE, fds); + if (rc < 0) return rc; + *master_fd = fds[0]; + *slave_fd = fds[1]; + return 0; +}