diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index a91de18..583c7b2 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -20,6 +20,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -303,6 +306,45 @@ namespace Zenith { Net::Socket::Close(fd, Sched::GetCurrentPid()); } + static int Sys_SendTo(int fd, const uint8_t* data, uint32_t len, + uint32_t destIp, uint16_t destPort) { + return Net::Socket::SendTo(fd, data, len, destIp, destPort, Sched::GetCurrentPid()); + } + + static int Sys_RecvFrom(int fd, uint8_t* buf, uint32_t maxLen, + uint32_t* srcIp, uint16_t* srcPort) { + return Net::Socket::RecvFrom(fd, buf, maxLen, srcIp, srcPort, Sched::GetCurrentPid()); + } + + static void Sys_GetNetCfg(NetCfg* out) { + if (out == nullptr) return; + out->ipAddress = Net::GetIpAddress(); + out->subnetMask = Net::GetSubnetMask(); + out->gateway = Net::GetGateway(); + + const uint8_t* mac = nullptr; + if (Drivers::Net::E1000::IsInitialized()) { + mac = Drivers::Net::E1000::GetMacAddress(); + } else if (Drivers::Net::E1000E::IsInitialized()) { + mac = Drivers::Net::E1000E::GetMacAddress(); + } + if (mac) { + for (int i = 0; i < 6; i++) out->macAddress[i] = mac[i]; + } else { + for (int i = 0; i < 6; i++) out->macAddress[i] = 0; + } + out->_pad[0] = 0; + out->_pad[1] = 0; + } + + static int Sys_SetNetCfg(const NetCfg* in) { + if (in == nullptr) return -1; + Net::SetIpAddress(in->ipAddress); + Net::SetSubnetMask(in->subnetMask); + Net::SetGateway(in->gateway); + return 0; + } + static void Sys_Reset() { /* Triple fault for now; TODO: implement UEFI runtime function for clean reboot. @@ -320,6 +362,16 @@ namespace Zenith { __builtin_unreachable(); } + // ---- File write/create ---- + + static int Sys_FWrite(int handle, const uint8_t* data, uint64_t offset, uint64_t size) { + return Fs::Vfs::VfsWrite(handle, data, offset, size); + } + + static int Sys_FCreate(const char* path) { + return Fs::Vfs::VfsCreate(path); + } + // ---- Dispatch ---- extern "C" int64_t SyscallDispatch(SyscallFrame* frame) { @@ -416,6 +468,24 @@ namespace Zenith { case SYS_CLOSESOCK: Sys_CloseSock((int)frame->arg1); return 0; + case SYS_GETNETCFG: + Sys_GetNetCfg((NetCfg*)frame->arg1); + return 0; + case SYS_SETNETCFG: + return (int64_t)Sys_SetNetCfg((const NetCfg*)frame->arg1); + case SYS_SENDTO: + return (int64_t)Sys_SendTo((int)frame->arg1, (const uint8_t*)frame->arg2, + (uint32_t)frame->arg3, (uint32_t)frame->arg4, + (uint16_t)frame->arg5); + case SYS_RECVFROM: + return (int64_t)Sys_RecvFrom((int)frame->arg1, (uint8_t*)frame->arg2, + (uint32_t)frame->arg3, (uint32_t*)frame->arg4, + (uint16_t*)frame->arg5); + case SYS_FWRITE: + return (int64_t)Sys_FWrite((int)frame->arg1, (const uint8_t*)frame->arg2, + frame->arg3, frame->arg4); + case SYS_FCREATE: + return (int64_t)Sys_FCreate((const char*)frame->arg1); default: return -1; } @@ -442,7 +512,7 @@ namespace Zenith { Hal::WriteMSR(Hal::IA32_FMASK, 0x200); Kt::KernelLogStream(Kt::OK, "Syscall") << "SYSCALL/SYSRET initialized (LSTAR=" - << kcp::hex << (uint64_t)SyscallEntry << kcp::dec << ", 37 syscalls)"; + << kcp::hex << (uint64_t)SyscallEntry << kcp::dec << ", 43 syscalls)"; } } diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index 9b03f4a..38fc400 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -48,8 +48,15 @@ namespace Zenith { static constexpr uint64_t SYS_SEND = 34; static constexpr uint64_t SYS_RECV = 35; static constexpr uint64_t SYS_CLOSESOCK = 36; + static constexpr uint64_t SYS_GETNETCFG = 37; + static constexpr uint64_t SYS_SETNETCFG = 38; + static constexpr uint64_t SYS_SENDTO = 39; + static constexpr uint64_t SYS_RECVFROM = 40; + static constexpr uint64_t SYS_FWRITE = 41; + static constexpr uint64_t SYS_FCREATE = 42; static constexpr int SOCK_TCP = 1; + static constexpr int SOCK_UDP = 2; struct DateTime { uint16_t Year; @@ -75,6 +82,14 @@ namespace Zenith { uint32_t maxProcesses; }; + struct NetCfg { + uint32_t ipAddress; // network byte order + uint32_t subnetMask; // network byte order + uint32_t gateway; // network byte order + uint8_t macAddress[6]; + uint8_t _pad[2]; + }; + struct KeyEvent { uint8_t scancode; char ascii; diff --git a/kernel/src/Drivers/Net/E1000E.cpp b/kernel/src/Drivers/Net/E1000E.cpp new file mode 100644 index 0000000..6d7b766 --- /dev/null +++ b/kernel/src/Drivers/Net/E1000E.cpp @@ -0,0 +1,763 @@ +/* + * E1000E.cpp + * Intel I217/I218/I219 (E1000E) Ethernet driver + * Copyright (c) 2025 Daniel Hammer +*/ + +#include "E1000E.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Kt; + +namespace Drivers::Net::E1000E { + + // Supported device entry + struct DeviceEntry { + uint16_t DeviceId; + const char* Name; + }; + + // PCI vendor ID for Intel + static constexpr uint16_t VendorIntel = 0x8086; + + // Device ID table for I217/I218/I219 series + static constexpr DeviceEntry g_deviceTable[] = { + // I217 + { 0x153A, "I217-LM" }, + { 0x153B, "I217-V" }, + // I218 + { 0x155A, "I218-LM" }, + { 0x1559, "I218-V" }, + { 0x15A0, "I218-LM (2)" }, + { 0x15A1, "I218-V (2)" }, + { 0x15A2, "I218-LM (3)" }, + { 0x15A3, "I218-V (3)" }, + // I219-LM variants + { 0x156F, "I219-LM" }, + { 0x15B7, "I219-LM (2)" }, + { 0x15BB, "I219-LM (3)" }, + { 0x15BD, "I219-LM (4)" }, + { 0x15DF, "I219-LM (5)" }, + { 0x15E1, "I219-LM (6)" }, + { 0x15E3, "I219-LM (7)" }, + { 0x15D7, "I219-LM (8)" }, + { 0x0D4C, "I219-LM (9)" }, + { 0x0D4E, "I219-LM (10)" }, + { 0x0D53, "I219-LM (11)" }, + { 0x0D55, "I219-LM (12)" }, + { 0x0DC5, "I219-LM (13)" }, + { 0x0DC7, "I219-LM (14)" }, + { 0x1A1C, "I219-LM (15)" }, + { 0x1A1E, "I219-LM (16)" }, + // I219-V variants + { 0x1570, "I219-V" }, + { 0x15B8, "I219-V (2)" }, + { 0x15BC, "I219-V (3)" }, + { 0x15BE, "I219-V (4)" }, + { 0x15E0, "I219-V (5)" }, + { 0x15E2, "I219-V (6)" }, + { 0x15D6, "I219-V (7)" }, + { 0x15D8, "I219-V (8)" }, + { 0x0D4D, "I219-V (9)" }, + { 0x0D4F, "I219-V (10)" }, + { 0x0D54, "I219-V (11)" }, + { 0x0DC6, "I219-V (13)" }, + { 0x0DC8, "I219-V (14)" }, + { 0x1A1D, "I219-V (15)" }, + { 0x1A1F, "I219-V (16)" }, + }; + + static constexpr uint32_t g_deviceTableSize = sizeof(g_deviceTable) / sizeof(g_deviceTable[0]); + + // PCI config space offsets + static constexpr uint8_t PCI_REG_BAR0 = 0x10; + static constexpr uint8_t PCI_REG_BAR1 = 0x14; + static constexpr uint8_t PCI_REG_COMMAND = 0x04; + static constexpr uint8_t PCI_REG_INTERRUPT = 0x3C; + + // PCI command register bits + static constexpr uint16_t PCI_CMD_BUS_MASTER = (1 << 2); + static constexpr uint16_t PCI_CMD_MEM_SPACE = (1 << 1); + static constexpr uint16_t PCI_CMD_INTX_DISABLE = (1 << 10); + + // MSI configuration + static constexpr uint8_t MSI_IRQ = 24; // IRQ slot 24 = vector 56 (first MSI slot) + static constexpr uint32_t MSI_VECTOR = 56; // IRQ_VECTOR_BASE + MSI_IRQ + static constexpr uint32_t MSI_ADDR_BASE = 0xFEE00000; // Local APIC message address range + + // Driver state + static bool g_initialized = false; + static bool g_pollingMode = false; + static volatile uint8_t* g_mmioBase = nullptr; + static uint8_t g_macAddress[6] = {}; + static uint8_t g_irqLine = 0; + + // Descriptor rings (physical addresses for DMA, virtual for CPU access) + static RxDescriptor* g_rxDescs = nullptr; + static TxDescriptor* g_txDescs = nullptr; + static uint64_t g_rxDescsPhys = 0; + static uint64_t g_txDescsPhys = 0; + + // Packet buffers (virtual addresses) + static uint8_t* g_rxBuffers[RX_DESC_COUNT] = {}; + static uint8_t* g_txBuffers[TX_DESC_COUNT] = {}; + static uint64_t g_rxBuffersPhys[RX_DESC_COUNT] = {}; + static uint64_t g_txBuffersPhys[TX_DESC_COUNT] = {}; + + // Current descriptor indices + static uint32_t g_rxTail = 0; + static uint32_t g_txTail = 0; + + // Statistics + static uint64_t g_rxPacketCount = 0; + static uint64_t g_txPacketCount = 0; + + // RX callback + static RxCallback g_rxCallback = nullptr; + + // ------------------------------------------------------------------------- + // Register access helpers + // ------------------------------------------------------------------------- + + static void WriteReg(uint32_t reg, uint32_t value) { + *(volatile uint32_t*)(g_mmioBase + reg) = value; + } + + static uint32_t ReadReg(uint32_t reg) { + return *(volatile uint32_t*)(g_mmioBase + reg); + } + + // ------------------------------------------------------------------------- + // SW/FW semaphore (prevents conflicts with Intel Management Engine) + // ------------------------------------------------------------------------- + + static bool AcquireSwFwSync() { + // Step 1: Acquire the software semaphore (SWSM.SMBI) + bool gotSmbi = false; + for (int i = 0; i < 2000; i++) { + uint32_t swsm = ReadReg(REG_SWSM); + if (!(swsm & SWSM_SMBI)) { + WriteReg(REG_SWSM, swsm | SWSM_SMBI); + if (ReadReg(REG_SWSM) & SWSM_SMBI) { + gotSmbi = true; + break; + } + } + } + + if (!gotSmbi) { + KernelLogStream(WARNING, "E1000E") << "Could not acquire SMBI, proceeding anyway"; + return false; + } + + // Step 2: Acquire the SW/FW semaphore (EXTCNF_CTRL.SW_OWN) + for (int i = 0; i < 2000; i++) { + uint32_t extcnf = ReadReg(REG_EXTCNF_CTRL); + if (!(extcnf & EXTCNF_CTRL_SWFLAG)) { + WriteReg(REG_EXTCNF_CTRL, extcnf | EXTCNF_CTRL_SWFLAG); + if (ReadReg(REG_EXTCNF_CTRL) & EXTCNF_CTRL_SWFLAG) { + return true; + } + } + } + + // Failed to acquire SWFLAG — release SMBI + uint32_t swsm = ReadReg(REG_SWSM); + WriteReg(REG_SWSM, swsm & ~SWSM_SMBI); + KernelLogStream(WARNING, "E1000E") << "Could not acquire SWFLAG, proceeding anyway"; + return false; + } + + static void ReleaseSwFwSync() { + uint32_t extcnf = ReadReg(REG_EXTCNF_CTRL); + WriteReg(REG_EXTCNF_CTRL, extcnf & ~EXTCNF_CTRL_SWFLAG); + + uint32_t swsm = ReadReg(REG_SWSM); + WriteReg(REG_SWSM, swsm & ~SWSM_SMBI); + } + + // ------------------------------------------------------------------------- + // PHY access via MDIC register + // ------------------------------------------------------------------------- + + static uint16_t PhyRead(uint32_t phyReg) { + // PHY address = 1 for internal PHY + uint32_t mdic = ((phyReg & 0x1F) << MDIC_REG_SHIFT) + | (1u << MDIC_PHY_SHIFT) + | MDIC_OP_READ; + + WriteReg(REG_MDIC, mdic); + + for (int i = 0; i < 200000; i++) { + mdic = ReadReg(REG_MDIC); + if (mdic & MDIC_READY) { + if (mdic & MDIC_ERROR) { + KernelLogStream(WARNING, "E1000E") << "PHY read error for reg " << base::hex << (uint64_t)phyReg; + return 0; + } + return (uint16_t)(mdic & MDIC_DATA_MASK); + } + } + + KernelLogStream(WARNING, "E1000E") << "PHY read timeout for reg " << base::hex << (uint64_t)phyReg; + return 0; + } + + static void PhyWrite(uint32_t phyReg, uint16_t value) { + uint32_t mdic = (uint32_t)value + | ((phyReg & 0x1F) << MDIC_REG_SHIFT) + | (1u << MDIC_PHY_SHIFT) + | MDIC_OP_WRITE; + + WriteReg(REG_MDIC, mdic); + + for (int i = 0; i < 200000; i++) { + mdic = ReadReg(REG_MDIC); + if (mdic & MDIC_READY) { + if (mdic & MDIC_ERROR) { + KernelLogStream(WARNING, "E1000E") << "PHY write error for reg " << base::hex << (uint64_t)phyReg; + } + return; + } + } + + KernelLogStream(WARNING, "E1000E") << "PHY write timeout for reg " << base::hex << (uint64_t)phyReg; + } + + // ------------------------------------------------------------------------- + // PHY initialization + // ------------------------------------------------------------------------- + + static void InitPhy() { + // Reset the PHY + PhyWrite(PHY_CONTROL, PHY_CTRL_RESET); + + // Wait for reset to complete + for (int i = 0; i < 100000; i++) { + uint16_t ctrl = PhyRead(PHY_CONTROL); + if (!(ctrl & PHY_CTRL_RESET)) { + break; + } + } + + // Advertise 10/100/1000 Mbps capabilities + uint16_t anar = PhyRead(PHY_AUTONEG_ADV); + anar |= (1 << 5) | (1 << 6) | (1 << 7) | (1 << 8); // 10/100 HD/FD + PhyWrite(PHY_AUTONEG_ADV, anar); + + // Advertise 1000BASE-T + uint16_t gbcr = PhyRead(PHY_1000T_CTRL); + gbcr |= (1 << 8) | (1 << 9); // 1000BASE-T HD/FD + PhyWrite(PHY_1000T_CTRL, gbcr); + + // Enable and restart auto-negotiation + PhyWrite(PHY_CONTROL, PHY_CTRL_AUTONEG_EN | PHY_CTRL_RESTART_AN); + + KernelLogStream(OK, "E1000E") << "PHY initialized, auto-negotiation started"; + } + + // ------------------------------------------------------------------------- + // EEPROM access (e1000e encoding differs from e1000) + // ------------------------------------------------------------------------- + + static uint16_t EepromRead(uint8_t address) { + // E1000E: address shifted left by 2 (not 8), done bit at position 1 (not 4) + WriteReg(REG_EERD, ((uint32_t)address << 2) | 1); + + uint32_t value; + for (int i = 0; i < 10000; i++) { + value = ReadReg(REG_EERD); + if (value & (1 << 1)) { + return (uint16_t)(value >> 16); + } + } + + KernelLogStream(WARNING, "E1000E") << "EEPROM read timeout for address " << base::hex << (uint64_t)address; + return 0; + } + + // ------------------------------------------------------------------------- + // MAC address + // ------------------------------------------------------------------------- + + static void ReadMacAddress() { + // Try reading from RAL/RAH first + uint32_t ral = ReadReg(REG_RAL); + uint32_t rah = ReadReg(REG_RAH); + + if (ral != 0) { + g_macAddress[0] = (uint8_t)(ral); + g_macAddress[1] = (uint8_t)(ral >> 8); + g_macAddress[2] = (uint8_t)(ral >> 16); + g_macAddress[3] = (uint8_t)(ral >> 24); + g_macAddress[4] = (uint8_t)(rah); + g_macAddress[5] = (uint8_t)(rah >> 8); + } else { + // Fallback: read from EEPROM + uint16_t word0 = EepromRead(0); + uint16_t word1 = EepromRead(1); + uint16_t word2 = EepromRead(2); + + g_macAddress[0] = (uint8_t)(word0); + g_macAddress[1] = (uint8_t)(word0 >> 8); + g_macAddress[2] = (uint8_t)(word1); + g_macAddress[3] = (uint8_t)(word1 >> 8); + g_macAddress[4] = (uint8_t)(word2); + g_macAddress[5] = (uint8_t)(word2 >> 8); + } + + // Write MAC back to RAL/RAH to ensure the filter is set + WriteReg(REG_RAL, + (uint32_t)g_macAddress[0] | + ((uint32_t)g_macAddress[1] << 8) | + ((uint32_t)g_macAddress[2] << 16) | + ((uint32_t)g_macAddress[3] << 24)); + WriteReg(REG_RAH, + (uint32_t)g_macAddress[4] | + ((uint32_t)g_macAddress[5] << 8) | + (1u << 31)); // AV (Address Valid) bit + } + + // ------------------------------------------------------------------------- + // Allocate page-aligned DMA buffer, returns virtual address + // ------------------------------------------------------------------------- + + static uint8_t* AllocateDmaBuffer(uint64_t& outPhysAddr) { + void* virt = Memory::g_pfa->AllocateZeroed(); + outPhysAddr = Memory::SubHHDM(virt); + return (uint8_t*)virt; + } + + // ------------------------------------------------------------------------- + // RX setup + // ------------------------------------------------------------------------- + + static void SetupRx() { + uint64_t descPhys; + g_rxDescs = (RxDescriptor*)AllocateDmaBuffer(descPhys); + g_rxDescsPhys = descPhys; + + for (uint32_t i = 0; i < RX_DESC_COUNT; i++) { + g_rxBuffers[i] = AllocateDmaBuffer(g_rxBuffersPhys[i]); + + // Allocate second page for larger buffer support + uint64_t secondPhys; + AllocateDmaBuffer(secondPhys); + + g_rxDescs[i].BufferAddress = g_rxBuffersPhys[i]; + g_rxDescs[i].Status = 0; + g_rxDescs[i].Length = 0; + g_rxDescs[i].Checksum = 0; + g_rxDescs[i].Errors = 0; + g_rxDescs[i].Special = 0; + } + + WriteReg(REG_RDBAL, (uint32_t)(g_rxDescsPhys & 0xFFFFFFFF)); + WriteReg(REG_RDBAH, (uint32_t)(g_rxDescsPhys >> 32)); + WriteReg(REG_RDLEN, RX_DESC_COUNT * sizeof(RxDescriptor)); + WriteReg(REG_RDH, 0); + WriteReg(REG_RDT, RX_DESC_COUNT - 1); + + g_rxTail = RX_DESC_COUNT - 1; + + uint32_t rctl = RCTL_EN | RCTL_BAM | RCTL_SECRC | RCTL_BSIZE_4096 | RCTL_BSEX; + WriteReg(REG_RCTL, rctl); + + KernelLogStream(OK, "E1000E") << "RX ring configured: " << base::dec << (uint64_t)RX_DESC_COUNT << " descriptors"; + } + + // ------------------------------------------------------------------------- + // TX setup + // ------------------------------------------------------------------------- + + static void SetupTx() { + uint64_t descPhys; + g_txDescs = (TxDescriptor*)AllocateDmaBuffer(descPhys); + g_txDescsPhys = descPhys; + + for (uint32_t i = 0; i < TX_DESC_COUNT; i++) { + g_txBuffers[i] = AllocateDmaBuffer(g_txBuffersPhys[i]); + + g_txDescs[i].BufferAddress = g_txBuffersPhys[i]; + g_txDescs[i].Length = 0; + g_txDescs[i].Command = 0; + g_txDescs[i].Status = TXSTA_DD; + g_txDescs[i].ChecksumOffset = 0; + g_txDescs[i].ChecksumStart = 0; + g_txDescs[i].Special = 0; + } + + WriteReg(REG_TDBAL, (uint32_t)(g_txDescsPhys & 0xFFFFFFFF)); + WriteReg(REG_TDBAH, (uint32_t)(g_txDescsPhys >> 32)); + WriteReg(REG_TDLEN, TX_DESC_COUNT * sizeof(TxDescriptor)); + WriteReg(REG_TDH, 0); + WriteReg(REG_TDT, 0); + + g_txTail = 0; + + uint32_t tctl = TCTL_EN | TCTL_PSP + | (15u << TCTL_CT_SHIFT) + | (64u << TCTL_COLD_SHIFT); + WriteReg(REG_TCTL, tctl); + + WriteReg(REG_TIPG, 10 | (10 << 10) | (10 << 20)); + + KernelLogStream(OK, "E1000E") << "TX ring configured: " << base::dec << (uint64_t)TX_DESC_COUNT << " descriptors"; + } + + // ------------------------------------------------------------------------- + // MSI setup + // ------------------------------------------------------------------------- + + static void HandleInterrupt(uint8_t irq); // forward declaration + + static bool SetupMsi(uint8_t bus, uint8_t dev, uint8_t func) { + uint8_t cap = Pci::FindCapability(bus, dev, func, Pci::PCI_CAP_MSI); + if (cap == 0) { + KernelLogStream(INFO, "E1000E") << "MSI capability not found"; + return false; + } + + KernelLogStream(INFO, "E1000E") << "MSI capability at offset " << base::hex << (uint64_t)cap; + + // Read Message Control (cap+2) + uint16_t msgCtrl = Pci::LegacyRead16(bus, dev, func, cap + 2); + bool is64bit = (msgCtrl & (1 << 7)) != 0; + + // Write Message Address (cap+4): BSP APIC ID 0, physical destination, fixed delivery + Pci::LegacyWrite32(bus, dev, func, cap + 4, MSI_ADDR_BASE); + + // Write Message Data (vector number, edge-triggered, fixed delivery) + if (is64bit) { + // 64-bit: Upper Address at cap+8, Data at cap+12 + Pci::LegacyWrite32(bus, dev, func, cap + 8, 0); + Pci::LegacyWrite16(bus, dev, func, cap + 12, MSI_VECTOR); + } else { + // 32-bit: Data at cap+8 + Pci::LegacyWrite16(bus, dev, func, cap + 8, MSI_VECTOR); + } + + // Enable MSI: set bit 0 (MSI Enable), clear bits 6:4 (single message) + msgCtrl &= ~(0x70); // Clear Multiple Message Enable (bits 6:4) + msgCtrl |= (1 << 0); // MSI Enable + Pci::LegacyWrite16(bus, dev, func, cap + 2, msgCtrl); + + // Disable legacy INTx in PCI command register + uint16_t pciCmd = Pci::LegacyRead16(bus, dev, func, PCI_REG_COMMAND); + pciCmd |= PCI_CMD_INTX_DISABLE; + Pci::LegacyWrite16(bus, dev, func, PCI_REG_COMMAND, pciCmd); + + // Register the interrupt handler for MSI vector + Hal::RegisterIrqHandler(MSI_IRQ, HandleInterrupt); + + KernelLogStream(OK, "E1000E") << "MSI enabled: vector " << base::dec << (uint64_t)MSI_VECTOR + << " (IRQ slot " << (uint64_t)MSI_IRQ << ")" << (is64bit ? " [64-bit]" : " [32-bit]"); + + return true; + } + + // ------------------------------------------------------------------------- + // Interrupt handler + // ------------------------------------------------------------------------- + + static void HandleInterrupt(uint8_t irq) { + (void)irq; + + uint32_t icr = ReadReg(REG_ICR); + + // Spurious IRQ guard + if (icr == 0) { + return; + } + + if (icr & ICR_LSC) { + uint32_t status = ReadReg(REG_STATUS); + bool linkUp = (status & (1 << 1)) != 0; + KernelLogStream(INFO, "E1000E") << "Link status change: " << (linkUp ? "UP" : "DOWN"); + } + + if (icr & ICR_RXT0) { + while (true) { + uint32_t nextIdx = (g_rxTail + 1) % RX_DESC_COUNT; + RxDescriptor& desc = g_rxDescs[nextIdx]; + + if (!(desc.Status & RXSTA_DD)) { + break; + } + + uint16_t length = desc.Length; + g_rxPacketCount++; + + if (g_rxCallback != nullptr) { + g_rxCallback(g_rxBuffers[nextIdx], length); + } + + desc.Status = 0; + desc.Length = 0; + desc.Errors = 0; + + g_rxTail = nextIdx; + WriteReg(REG_RDT, g_rxTail); + } + } + + if (icr & (ICR_TXDW | ICR_TXQE)) { + // TX completion — nothing to do for now + } + } + + // ------------------------------------------------------------------------- + // Public API + // ------------------------------------------------------------------------- + + void Initialize() { + KernelLogStream(INFO, "E1000E") << "Scanning for Intel e1000e NIC..."; + + auto& devices = Pci::GetDevices(); + const Pci::PciDevice* foundDev = nullptr; + const char* foundName = nullptr; + + for (uint64_t i = 0; i < devices.size(); i++) { + if (devices[i].VendorId != VendorIntel) { + continue; + } + for (uint32_t j = 0; j < g_deviceTableSize; j++) { + if (devices[i].DeviceId == g_deviceTable[j].DeviceId) { + foundDev = &devices[i]; + foundName = g_deviceTable[j].Name; + break; + } + } + if (foundDev != nullptr) { + break; + } + } + + if (foundDev == nullptr) { + KernelLogStream(WARNING, "E1000E") << "No e1000e NIC found"; + return; + } + + KernelLogStream(OK, "E1000E") << "Found " << foundName << " at PCI " + << base::hex << (uint64_t)foundDev->Bus << ":" + << (uint64_t)foundDev->Device << "." << (uint64_t)foundDev->Function; + + // Read BAR0 (MMIO base address), check for 64-bit BAR + uint32_t bar0 = Pci::LegacyRead32(foundDev->Bus, foundDev->Device, foundDev->Function, PCI_REG_BAR0); + uint64_t mmioPhys = bar0 & 0xFFFFFFF0; + + // Check if 64-bit BAR (type field bits 2:1 == 0b10) + if ((bar0 & 0x06) == 0x04) { + uint32_t bar1 = Pci::LegacyRead32(foundDev->Bus, foundDev->Device, foundDev->Function, PCI_REG_BAR1); + mmioPhys |= ((uint64_t)bar1 << 32); + } + + KernelLogStream(INFO, "E1000E") << "BAR0 physical: " << base::hex << mmioPhys; + + // Map the MMIO region (128KB = 32 pages) + constexpr uint64_t MmioSize = 0x20000; + for (uint64_t offset = 0; offset < MmioSize; offset += 0x1000) { + Memory::VMM::g_paging->MapMMIO(mmioPhys + offset, Memory::HHDM(mmioPhys + offset)); + } + + g_mmioBase = (volatile uint8_t*)Memory::HHDM(mmioPhys); + + // Enable bus mastering and memory space in PCI command register + uint16_t pciCmd = Pci::LegacyRead16(foundDev->Bus, foundDev->Device, foundDev->Function, PCI_REG_COMMAND); + pciCmd |= PCI_CMD_BUS_MASTER | PCI_CMD_MEM_SPACE; + Pci::LegacyWrite16(foundDev->Bus, foundDev->Device, foundDev->Function, PCI_REG_COMMAND, pciCmd); + + KernelLogStream(OK, "E1000E") << "Bus mastering enabled"; + + // Read interrupt line from PCI config (used for legacy IRQ fallback) + g_irqLine = Pci::LegacyRead8(foundDev->Bus, foundDev->Device, foundDev->Function, PCI_REG_INTERRUPT); + KernelLogStream(INFO, "E1000E") << "PCI IRQ line: " << base::dec << (uint64_t)g_irqLine; + + // --- ICH/PCH reset sequence --- + + // 1. Disable interrupts, flush ICR + KernelLogStream(INFO, "E1000E") << "Disabling interrupts..."; + WriteReg(REG_IMC, 0xFFFFFFFF); + ReadReg(REG_ICR); + + // 2. Acquire SW/FW semaphore (non-fatal if it fails) + KernelLogStream(INFO, "E1000E") << "Acquiring semaphore..."; + AcquireSwFwSync(); + + // 3. Reset the device + KernelLogStream(INFO, "E1000E") << "Resetting device..."; + uint32_t ctrl = ReadReg(REG_CTRL); + WriteReg(REG_CTRL, ctrl | CTRL_RST); + + // Post-reset settling delay (give hardware time before polling) + for (int i = 0; i < 100000; i++) { + asm volatile("" ::: "memory"); + } + + // Poll for reset completion + for (int i = 0; i < 10000; i++) { + if (!(ReadReg(REG_CTRL) & CTRL_RST)) { + break; + } + } + + // 4. Release semaphore + ReleaseSwFwSync(); + + // 5. Disable interrupts again after reset + WriteReg(REG_IMC, 0xFFFFFFFF); + ReadReg(REG_ICR); + + KernelLogStream(OK, "E1000E") << "Reset complete"; + + // Set link up: SLU on, but let auto-negotiation decide speed/duplex + ctrl = ReadReg(REG_CTRL); + ctrl |= CTRL_SLU; + ctrl &= ~CTRL_FRCSPD; + ctrl &= ~CTRL_FRCDPLX; + ctrl &= ~(1u << 3); // Clear LRST + ctrl &= ~(1u << 31); // Clear PHY_RST + ctrl &= ~(1u << 7); // Clear ILOS + WriteReg(REG_CTRL, ctrl); + + // Initialize PHY + InitPhy(); + + // Read MAC address + ReadMacAddress(); + + KernelLogStream(OK, "E1000E") << "MAC: " + << base::hex + << (uint64_t)g_macAddress[0] << ":" + << (uint64_t)g_macAddress[1] << ":" + << (uint64_t)g_macAddress[2] << ":" + << (uint64_t)g_macAddress[3] << ":" + << (uint64_t)g_macAddress[4] << ":" + << (uint64_t)g_macAddress[5]; + + // Zero out the Multicast Table Array (128 entries) + for (uint32_t i = 0; i < 128; i++) { + WriteReg(REG_MTA + (i * 4), 0); + } + + // Set up RX and TX descriptor rings + SetupRx(); + SetupTx(); + + // Three-tier interrupt strategy: MSI → legacy IRQ → polling + if (SetupMsi(foundDev->Bus, foundDev->Device, foundDev->Function)) { + // MSI configured — enable NIC interrupt causes + WriteReg(REG_IMS, ICR_RXT0 | ICR_TXDW | ICR_TXQE | ICR_LSC | ICR_RXDMT0); + } else if (g_irqLine != 0xFF) { + // Legacy IRQ fallback + KernelLogStream(INFO, "E1000E") << "Falling back to legacy IRQ " << base::dec << (uint64_t)g_irqLine; + Hal::RegisterIrqHandler(g_irqLine, HandleInterrupt); + Hal::IoApic::UnmaskIrq(Hal::IoApic::GetGsiForIrq(g_irqLine)); + WriteReg(REG_IMS, ICR_RXT0 | ICR_TXDW | ICR_TXQE | ICR_LSC | ICR_RXDMT0); + } else { + // Polling as last resort + KernelLogStream(WARNING, "E1000E") << "No MSI or legacy IRQ available, using polling mode"; + g_pollingMode = true; + } + + g_initialized = true; + + uint32_t status = ReadReg(REG_STATUS); + bool linkUp = (status & (1 << 1)) != 0; + KernelLogStream(OK, "E1000E") << "Initialization complete, link: " << (linkUp ? "UP" : "DOWN"); + } + + // ------------------------------------------------------------------------- + // Polling: process received packets (with reentrancy guard) + // ------------------------------------------------------------------------- + + static bool g_polling = false; + + static void PollRx() { + // Guard against reentrancy: RX callback can trigger ARP reply → + // Ethernet::Send → SendPacket → PollRx(). Two concurrent callers + // modifying g_rxTail / RDT would corrupt the descriptor ring. + if (g_polling) { + return; + } + g_polling = true; + + while (true) { + uint32_t nextIdx = (g_rxTail + 1) % RX_DESC_COUNT; + RxDescriptor& desc = g_rxDescs[nextIdx]; + + if (!(desc.Status & RXSTA_DD)) { + break; + } + + uint16_t length = desc.Length; + g_rxPacketCount++; + + if (g_rxCallback != nullptr) { + g_rxCallback(g_rxBuffers[nextIdx], length); + } + + desc.Status = 0; + desc.Length = 0; + desc.Errors = 0; + + g_rxTail = nextIdx; + WriteReg(REG_RDT, g_rxTail); + } + + g_polling = false; + } + + bool SendPacket(const uint8_t* data, uint16_t length) { + if (!g_initialized || data == nullptr || length == 0 || length > 1518) { + return false; + } + + TxDescriptor& desc = g_txDescs[g_txTail]; + if (!(desc.Status & TXSTA_DD)) { + KernelLogStream(WARNING, "E1000E") << "TX ring full"; + return false; + } + + memcpy(g_txBuffers[g_txTail], data, length); + + desc.BufferAddress = g_txBuffersPhys[g_txTail]; + desc.Length = length; + desc.Command = TXCMD_EOP | TXCMD_IFCS | TXCMD_RS; + desc.Status = 0; + + g_txTail = (g_txTail + 1) % TX_DESC_COUNT; + WriteReg(REG_TDT, g_txTail); + + g_txPacketCount++; + return true; + } + + const uint8_t* GetMacAddress() { + return g_macAddress; + } + + bool IsInitialized() { + return g_initialized; + } + + void SetRxCallback(RxCallback callback) { + g_rxCallback = callback; + } + + void Poll() { + if (!g_initialized) { + return; + } + PollRx(); + } + +}; diff --git a/kernel/src/Drivers/Net/E1000E.hpp b/kernel/src/Drivers/Net/E1000E.hpp new file mode 100644 index 0000000..9f996be --- /dev/null +++ b/kernel/src/Drivers/Net/E1000E.hpp @@ -0,0 +1,155 @@ +/* + * E1000E.hpp + * Intel I217/I218/I219 (E1000E) Ethernet driver + * Copyright (c) 2025 Daniel Hammer +*/ + +#pragma once +#include + +namespace Drivers::Net::E1000E { + + // E1000E register offsets (memory-mapped via BAR0) + constexpr uint32_t REG_CTRL = 0x0000; // Device Control + constexpr uint32_t REG_STATUS = 0x0008; // Device Status + constexpr uint32_t REG_EERD = 0x0014; // EEPROM Read + constexpr uint32_t REG_CTRL_EXT = 0x0018; // Extended Device Control + constexpr uint32_t REG_MDIC = 0x0020; // MDI Control (PHY access) + constexpr uint32_t REG_ICR = 0x00C0; // Interrupt Cause Read + constexpr uint32_t REG_IMS = 0x00D0; // Interrupt Mask Set + constexpr uint32_t REG_IMC = 0x00D8; // Interrupt Mask Clear + constexpr uint32_t REG_RCTL = 0x0100; // Receive Control + constexpr uint32_t REG_TCTL = 0x0400; // Transmit Control + constexpr uint32_t REG_TIPG = 0x0410; // Transmit IPG + constexpr uint32_t REG_RDBAL = 0x2800; // RX Descriptor Base Low + constexpr uint32_t REG_RDBAH = 0x2804; // RX Descriptor Base High + constexpr uint32_t REG_RDLEN = 0x2808; // RX Descriptor Length + constexpr uint32_t REG_RDH = 0x2810; // RX Descriptor Head + constexpr uint32_t REG_RDT = 0x2818; // RX Descriptor Tail + constexpr uint32_t REG_TDBAL = 0x3800; // TX Descriptor Base Low + constexpr uint32_t REG_TDBAH = 0x3804; // TX Descriptor Base High + constexpr uint32_t REG_TDLEN = 0x3808; // TX Descriptor Length + constexpr uint32_t REG_TDH = 0x3810; // TX Descriptor Head + constexpr uint32_t REG_TDT = 0x3818; // TX Descriptor Tail + constexpr uint32_t REG_MTA = 0x5200; // Multicast Table Array (128 entries) + constexpr uint32_t REG_RAL = 0x5400; // Receive Address Low + constexpr uint32_t REG_RAH = 0x5404; // Receive Address High + constexpr uint32_t REG_EXTCNF_CTRL = 0x0F00; // Extended Configuration Control + constexpr uint32_t REG_SWSM = 0x5B50; // Software Semaphore + constexpr uint32_t REG_FWSM = 0x5B54; // Firmware Semaphore + + // CTRL register bits + constexpr uint32_t CTRL_SLU = (1 << 6); // Set Link Up + constexpr uint32_t CTRL_FRCSPD = (1 << 11); // Force Speed + constexpr uint32_t CTRL_FRCDPLX = (1 << 12); // Force Duplex + constexpr uint32_t CTRL_RST = (1 << 26); // Device Reset + + // MDIC register + constexpr uint32_t MDIC_DATA_MASK = 0x0000FFFF; + constexpr uint32_t MDIC_REG_SHIFT = 16; // PHY register address shift + constexpr uint32_t MDIC_PHY_SHIFT = 21; // PHY address shift + constexpr uint32_t MDIC_OP_READ = (2u << 26); + constexpr uint32_t MDIC_OP_WRITE = (1u << 26); + constexpr uint32_t MDIC_READY = (1u << 28); + constexpr uint32_t MDIC_ERROR = (1u << 30); + + // PHY register addresses + constexpr uint32_t PHY_CONTROL = 0x00; // PHY Control Register + constexpr uint32_t PHY_STATUS = 0x01; // PHY Status Register + constexpr uint32_t PHY_AUTONEG_ADV = 0x04; // Auto-Negotiation Advertisement + constexpr uint32_t PHY_1000T_CTRL = 0x09; // 1000BASE-T Control + + // PHY Control register bits + constexpr uint16_t PHY_CTRL_RESET = (1 << 15); + constexpr uint16_t PHY_CTRL_AUTONEG_EN = (1 << 12); + constexpr uint16_t PHY_CTRL_RESTART_AN = (1 << 9); + + // Semaphore bits + constexpr uint32_t SWSM_SMBI = (1 << 0); + constexpr uint32_t SWSM_SWESMBI = (1 << 1); + constexpr uint32_t EXTCNF_CTRL_SWFLAG = (1 << 5); + + // RCTL register bits + constexpr uint32_t RCTL_EN = (1 << 1); // Receiver Enable + constexpr uint32_t RCTL_SBP = (1 << 2); // Store Bad Packets + constexpr uint32_t RCTL_UPE = (1 << 3); // Unicast Promiscuous + constexpr uint32_t RCTL_MPE = (1 << 4); // Multicast Promiscuous + constexpr uint32_t RCTL_BAM = (1 << 15); // Broadcast Accept Mode + constexpr uint32_t RCTL_BSIZE_4096 = (3 << 16); // Buffer Size 4096 (with BSEX) + constexpr uint32_t RCTL_BSEX = (1 << 25); // Buffer Size Extension + constexpr uint32_t RCTL_SECRC = (1 << 26); // Strip Ethernet CRC + + // TCTL register bits + constexpr uint32_t TCTL_EN = (1 << 1); // Transmit Enable + constexpr uint32_t TCTL_PSP = (1 << 3); // Pad Short Packets + constexpr uint32_t TCTL_CT_SHIFT = 4; // Collision Threshold shift + constexpr uint32_t TCTL_COLD_SHIFT = 12; // Collision Distance shift + + // ICR (interrupt cause) bits + constexpr uint32_t ICR_TXDW = (1 << 0); // TX Descriptor Written Back + constexpr uint32_t ICR_TXQE = (1 << 1); // TX Queue Empty + constexpr uint32_t ICR_LSC = (1 << 2); // Link Status Change + constexpr uint32_t ICR_RXDMT0 = (1 << 4); // RX Descriptor Minimum Threshold + constexpr uint32_t ICR_RXO = (1 << 6); // Receiver Overrun + constexpr uint32_t ICR_RXT0 = (1 << 7); // Receiver Timer Interrupt + + // TX descriptor command bits + constexpr uint8_t TXCMD_EOP = (1 << 0); // End Of Packet + constexpr uint8_t TXCMD_IFCS = (1 << 1); // Insert FCS/CRC + constexpr uint8_t TXCMD_RS = (1 << 3); // Report Status + + // TX descriptor status bits + constexpr uint8_t TXSTA_DD = (1 << 0); // Descriptor Done + + // RX descriptor status bits + constexpr uint8_t RXSTA_DD = (1 << 0); // Descriptor Done + constexpr uint8_t RXSTA_EOP = (1 << 1); // End Of Packet + + // Descriptor ring sizes + constexpr uint32_t RX_DESC_COUNT = 32; + constexpr uint32_t TX_DESC_COUNT = 32; + constexpr uint32_t PACKET_BUFFER_SIZE = 8192; + + // RX descriptor (legacy format, 16 bytes) + struct RxDescriptor { + uint64_t BufferAddress; + uint16_t Length; + uint16_t Checksum; + uint8_t Status; + uint8_t Errors; + uint16_t Special; + } __attribute__((packed)); + + // TX descriptor (legacy format, 16 bytes) + struct TxDescriptor { + uint64_t BufferAddress; + uint16_t Length; + uint8_t ChecksumOffset; + uint8_t Command; + uint8_t Status; + uint8_t ChecksumStart; + uint16_t Special; + } __attribute__((packed)); + + // Initialize the E1000E driver (scans PCI for the device) + void Initialize(); + + // Send a raw Ethernet frame + bool SendPacket(const uint8_t* data, uint16_t length); + + // Get the MAC address (6 bytes) + const uint8_t* GetMacAddress(); + + // Check if the device was found and initialized + bool IsInitialized(); + + // RX callback type: called with (packet data, length) + using RxCallback = void(*)(const uint8_t* data, uint16_t length); + + // Register a callback for received packets + void SetRxCallback(RxCallback callback); + + // Poll for received packets (used when legacy IRQ is unavailable) + void Poll(); + +}; diff --git a/kernel/src/Fs/Ramdisk.cpp b/kernel/src/Fs/Ramdisk.cpp index bcff151..abc4172 100644 --- a/kernel/src/Fs/Ramdisk.cpp +++ b/kernel/src/Fs/Ramdisk.cpp @@ -8,6 +8,7 @@ #include #include #include +#include namespace Fs::Ramdisk { @@ -105,6 +106,8 @@ namespace Fs::Ramdisk { entry.isDirectory = (typeFlag == '5'); entry.size = size; + entry.capacity = size; + entry.heapAllocated = false; // Data starts at next 512-byte block entry.data = ptr + 512; @@ -235,6 +238,110 @@ namespace Fs::Ramdisk { return count; } + int Write(int handle, const uint8_t* buffer, uint64_t offset, uint64_t size) { + if (handle < 0 || handle >= fileCount) return -1; + if (buffer == nullptr || size == 0) return 0; + + FileEntry& entry = fileTable[handle]; + if (entry.isDirectory) return -1; + + uint64_t endOffset = offset + size; + + // Copy-on-write: if data points into tar memory, copy to heap + if (!entry.heapAllocated) { + uint64_t newCap = entry.size; + if (endOffset > newCap) newCap = endOffset; + if (newCap < 256) newCap = 256; + // Round up to next power of 2 for growth + uint64_t rounded = 256; + while (rounded < newCap) rounded *= 2; + newCap = rounded; + + uint8_t* newBuf = (uint8_t*)Memory::g_heap->Request(newCap); + if (newBuf == nullptr) return -1; + + if (entry.data && entry.size > 0) { + memcpy(newBuf, entry.data, entry.size); + } + + entry.data = newBuf; + entry.capacity = newCap; + entry.heapAllocated = true; + } + + // Grow buffer if needed + if (endOffset > entry.capacity) { + uint64_t newCap = entry.capacity; + while (newCap < endOffset) newCap *= 2; + + uint8_t* newBuf = (uint8_t*)Memory::g_heap->Request(newCap); + if (newBuf == nullptr) return -1; + + if (entry.data && entry.size > 0) { + memcpy(newBuf, entry.data, entry.size); + } + Memory::g_heap->Free(entry.data); + + entry.data = newBuf; + entry.capacity = newCap; + } + + memcpy(entry.data + offset, buffer, size); + + if (endOffset > entry.size) { + entry.size = endOffset; + } + + return (int)size; + } + + int Create(const char* path) { + if (path == nullptr) return -1; + if (fileCount >= MaxFiles) return -1; + + // Normalize: skip leading '/' + if (path[0] == '/') path++; + + // Check if file already exists + for (int i = 0; i < fileCount; i++) { + if (StrEqual(fileTable[i].name, path)) { + // File exists — truncate it + FileEntry& entry = fileTable[i]; + if (!entry.heapAllocated) { + uint8_t* newBuf = (uint8_t*)Memory::g_heap->Request(256); + if (newBuf == nullptr) return -1; + entry.data = newBuf; + entry.capacity = 256; + entry.heapAllocated = true; + } + entry.size = 0; + entry.isDirectory = false; + return i; + } + } + + // Create new file entry + FileEntry& entry = fileTable[fileCount]; + + int nameLen = 0; + while (nameLen < MaxNameLen - 1 && path[nameLen] != '\0') { + entry.name[nameLen] = path[nameLen]; + nameLen++; + } + entry.name[nameLen] = '\0'; + + uint8_t* buf = (uint8_t*)Memory::g_heap->Request(256); + if (buf == nullptr) return -1; + + entry.data = buf; + entry.size = 0; + entry.capacity = 256; + entry.isDirectory = false; + entry.heapAllocated = true; + + return fileCount++; + } + int GetFileCount() { return fileCount; } diff --git a/kernel/src/Fs/Ramdisk.hpp b/kernel/src/Fs/Ramdisk.hpp index d68de54..09adf88 100644 --- a/kernel/src/Fs/Ramdisk.hpp +++ b/kernel/src/Fs/Ramdisk.hpp @@ -17,13 +17,17 @@ namespace Fs::Ramdisk { char name[MaxNameLen]; uint8_t* data; uint64_t size; + uint64_t capacity; bool isDirectory; + bool heapAllocated; }; void Initialize(void* moduleData, uint64_t moduleSize); int Open(const char* path); int Read(int handle, uint8_t* buffer, uint64_t offset, uint64_t size); + int Write(int handle, const uint8_t* buffer, uint64_t offset, uint64_t size); + int Create(const char* path); uint64_t GetSize(int handle); void Close(int handle); diff --git a/kernel/src/Fs/Vfs.cpp b/kernel/src/Fs/Vfs.cpp index 7fd0f07..1f19e85 100644 --- a/kernel/src/Fs/Vfs.cpp +++ b/kernel/src/Fs/Vfs.cpp @@ -123,6 +123,46 @@ namespace Fs::Vfs { entry.inUse = false; } + int VfsWrite(int handle, const uint8_t* buffer, uint64_t offset, uint64_t size) { + if (handle < 0 || handle >= MaxHandles || !handleTable[handle].inUse) return -1; + + HandleEntry& entry = handleTable[handle]; + if (driveTable[entry.driveNumber]->Write == nullptr) return -1; + return driveTable[entry.driveNumber]->Write(entry.localHandle, buffer, offset, size); + } + + int VfsCreate(const char* path) { + int drive; + const char* localPath; + + if (!ParsePath(path, drive, localPath)) { + Kt::KernelLogStream(Kt::ERROR, "VFS") << "Invalid path format for Create"; + return -1; + } + + if (drive < 0 || drive >= MaxDrives || driveTable[drive] == nullptr) { + Kt::KernelLogStream(Kt::ERROR, "VFS") << "Drive " << drive << " not registered"; + return -1; + } + + if (driveTable[drive]->Create == nullptr) return -1; + + int localHandle = driveTable[drive]->Create(localPath); + if (localHandle < 0) return -1; + + int globalHandle = AllocHandle(); + if (globalHandle < 0) { + Kt::KernelLogStream(Kt::ERROR, "VFS") << "No free handles"; + return -1; + } + + handleTable[globalHandle].inUse = true; + handleTable[globalHandle].driveNumber = drive; + handleTable[globalHandle].localHandle = localHandle; + + return globalHandle; + } + int VfsReadDir(const char* path, const char** outNames, int maxEntries) { int drive; const char* localPath; diff --git a/kernel/src/Fs/Vfs.hpp b/kernel/src/Fs/Vfs.hpp index 2c6b88b..1c60f80 100644 --- a/kernel/src/Fs/Vfs.hpp +++ b/kernel/src/Fs/Vfs.hpp @@ -19,6 +19,8 @@ namespace Fs::Vfs { uint64_t (*GetSize)(int handle); void (*Close)(int handle); int (*ReadDir)(const char* path, const char** outNames, int maxEntries); + int (*Write)(int handle, const uint8_t* buffer, uint64_t offset, uint64_t size); + int (*Create)(const char* path); }; void Initialize(); @@ -26,6 +28,8 @@ namespace Fs::Vfs { int VfsOpen(const char* path); int VfsRead(int handle, uint8_t* buffer, uint64_t offset, uint64_t size); + int VfsWrite(int handle, const uint8_t* buffer, uint64_t offset, uint64_t size); + int VfsCreate(const char* path); uint64_t VfsGetSize(int handle); void VfsClose(int handle); int VfsReadDir(const char* path, const char** outNames, int maxEntries); diff --git a/kernel/src/Hal/Apic/Interrupts.hpp b/kernel/src/Hal/Apic/Interrupts.hpp index 1e7f170..62753e7 100644 --- a/kernel/src/Hal/Apic/Interrupts.hpp +++ b/kernel/src/Hal/Apic/Interrupts.hpp @@ -8,11 +8,11 @@ #include namespace Hal { - // IRQ handler function type. The parameter is the IRQ number (0-23). + // IRQ handler function type. The parameter is the IRQ number (0-47). using IrqHandler = void(*)(uint8_t irq); - // Number of IRQ lines supported (IOAPIC inputs) - constexpr int IRQ_COUNT = 24; + // Number of IRQ slots supported (0-23: legacy ISA via IOAPIC, 24-47: MSI) + constexpr int IRQ_COUNT = 48; // IRQ vector base: hardware IRQs start at IDT vector 32 constexpr uint8_t IRQ_VECTOR_BASE = 32; @@ -29,7 +29,7 @@ namespace Hal { constexpr uint8_t IRQ_ATA1 = 14; constexpr uint8_t IRQ_ATA2 = 15; - // Register a handler for the given IRQ number (0-23) + // Register a handler for the given IRQ number (0-47) void RegisterIrqHandler(uint8_t irq, IrqHandler handler); // Install IRQ stubs into the IDT and set up the dispatch table diff --git a/kernel/src/Hal/Apic/IsrStubs.asm b/kernel/src/Hal/Apic/IsrStubs.asm index 47cf4a6..ca5b900 100644 --- a/kernel/src/Hal/Apic/IsrStubs.asm +++ b/kernel/src/Hal/Apic/IsrStubs.asm @@ -65,8 +65,8 @@ IrqCommon: iretq -; Define stubs for IRQs 0..23 (vectors 32..55) -; This covers all standard ISA IRQs plus some extra IOAPIC inputs +; Define stubs for IRQs 0..47 (vectors 32..79) +; 0-23: legacy ISA IRQs via IOAPIC, 24-47: MSI vectors IRQ_STUB 0 IRQ_STUB 1 IRQ_STUB 2 @@ -91,6 +91,30 @@ IRQ_STUB 20 IRQ_STUB 21 IRQ_STUB 22 IRQ_STUB 23 +IRQ_STUB 24 +IRQ_STUB 25 +IRQ_STUB 26 +IRQ_STUB 27 +IRQ_STUB 28 +IRQ_STUB 29 +IRQ_STUB 30 +IRQ_STUB 31 +IRQ_STUB 32 +IRQ_STUB 33 +IRQ_STUB 34 +IRQ_STUB 35 +IRQ_STUB 36 +IRQ_STUB 37 +IRQ_STUB 38 +IRQ_STUB 39 +IRQ_STUB 40 +IRQ_STUB 41 +IRQ_STUB 42 +IRQ_STUB 43 +IRQ_STUB 44 +IRQ_STUB 45 +IRQ_STUB 46 +IRQ_STUB 47 ; Spurious interrupt handler (vector 0xFF) - do nothing, no EOI global IrqStubSpurious @@ -125,3 +149,27 @@ IrqStubTable: dq IrqStub21 dq IrqStub22 dq IrqStub23 + dq IrqStub24 + dq IrqStub25 + dq IrqStub26 + dq IrqStub27 + dq IrqStub28 + dq IrqStub29 + dq IrqStub30 + dq IrqStub31 + dq IrqStub32 + dq IrqStub33 + dq IrqStub34 + dq IrqStub35 + dq IrqStub36 + dq IrqStub37 + dq IrqStub38 + dq IrqStub39 + dq IrqStub40 + dq IrqStub41 + dq IrqStub42 + dq IrqStub43 + dq IrqStub44 + dq IrqStub45 + dq IrqStub46 + dq IrqStub47 diff --git a/kernel/src/Main.cpp b/kernel/src/Main.cpp index d52daeb..49ddec0 100644 --- a/kernel/src/Main.cpp +++ b/kernel/src/Main.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -153,6 +154,10 @@ extern "C" void kmain() { Drivers::PS2::Mouse::Initialize(); Drivers::Net::E1000::Initialize(); + if (!Drivers::Net::E1000::IsInitialized()) { + KernelLogStream(INFO, "Init") << "E1000 not found, trying E1000E..."; + Drivers::Net::E1000E::Initialize(); + } Net::Initialize(); } #endif @@ -188,7 +193,9 @@ extern "C" void kmain() { Fs::Ramdisk::Read, Fs::Ramdisk::GetSize, Fs::Ramdisk::Close, - Fs::Ramdisk::ReadDir + Fs::Ramdisk::ReadDir, + Fs::Ramdisk::Write, + Fs::Ramdisk::Create }; Fs::Vfs::RegisterDrive(0, &ramdiskDriver); @@ -198,7 +205,7 @@ extern "C" void kmain() { Zenith::InitializeSyscalls(); Sched::Initialize(); - Sched::Spawn("0:/shell.elf"); + Sched::Spawn("0:/os/init.elf"); // Enable preemptive scheduling via the APIC timer Timekeeping::EnableSchedulerTick(); diff --git a/kernel/src/Net/Arp.cpp b/kernel/src/Net/Arp.cpp index f98e0da..934c4c9 100644 --- a/kernel/src/Net/Arp.cpp +++ b/kernel/src/Net/Arp.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -19,6 +20,12 @@ using namespace Kt; namespace Net::Arp { + static const uint8_t* GetActiveNicMac() { + if (Drivers::Net::E1000::IsInitialized()) + return Drivers::Net::E1000::GetMacAddress(); + return Drivers::Net::E1000E::GetMacAddress(); + } + // ARP cache entry struct CacheEntry { uint32_t Ip; @@ -107,7 +114,7 @@ namespace Net::Arp { reply.ProtocolAddrLen = 4; reply.Operation = Htons(OP_REPLY); - memcpy(reply.SenderMac, Drivers::Net::E1000::GetMacAddress(), 6); + memcpy(reply.SenderMac, GetActiveNicMac(), 6); reply.SenderIp = GetIpAddress(); memcpy(reply.TargetMac, pkt->SenderMac, 6); reply.TargetIp = senderIp; @@ -141,7 +148,7 @@ namespace Net::Arp { req.ProtocolAddrLen = 4; req.Operation = Htons(OP_REQUEST); - memcpy(req.SenderMac, Drivers::Net::E1000::GetMacAddress(), 6); + memcpy(req.SenderMac, GetActiveNicMac(), 6); req.SenderIp = GetIpAddress(); memset(req.TargetMac, 0, 6); req.TargetIp = targetIp; diff --git a/kernel/src/Net/Ethernet.cpp b/kernel/src/Net/Ethernet.cpp index 0761e91..7590f79 100644 --- a/kernel/src/Net/Ethernet.cpp +++ b/kernel/src/Net/Ethernet.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -17,6 +18,18 @@ using namespace Kt; namespace Net::Ethernet { + static const uint8_t* GetActiveNicMac() { + if (Drivers::Net::E1000::IsInitialized()) + return Drivers::Net::E1000::GetMacAddress(); + return Drivers::Net::E1000E::GetMacAddress(); + } + + static bool ActiveNicSend(const uint8_t* data, uint16_t length) { + if (Drivers::Net::E1000::IsInitialized()) + return Drivers::Net::E1000::SendPacket(data, length); + return Drivers::Net::E1000E::SendPacket(data, length); + } + void Initialize() { KernelLogStream(OK, "Net") << "Ethernet layer initialized"; } @@ -30,14 +43,14 @@ namespace Net::Ethernet { Header* hdr = (Header*)frame; memcpy(hdr->DestMac, destMac, 6); - memcpy(hdr->SrcMac, Drivers::Net::E1000::GetMacAddress(), 6); + memcpy(hdr->SrcMac, GetActiveNicMac(), 6); hdr->EtherType = Htons(etherType); memcpy(frame + HEADER_SIZE, payload, payloadLen); uint16_t totalLen = HEADER_SIZE + payloadLen; - return Drivers::Net::E1000::SendPacket(frame, totalLen); + return ActiveNicSend(frame, totalLen); } void OnFrameReceived(const uint8_t* data, uint16_t length) { diff --git a/kernel/src/Net/Net.cpp b/kernel/src/Net/Net.cpp index 23af4cd..43a88d7 100644 --- a/kernel/src/Net/Net.cpp +++ b/kernel/src/Net/Net.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -22,8 +23,8 @@ using namespace Kt; namespace Net { void Initialize() { - if (!Drivers::Net::E1000::IsInitialized()) { - KernelLogStream(WARNING, "Net") << "E1000 not initialized, skipping network stack"; + if (!Drivers::Net::E1000::IsInitialized() && !Drivers::Net::E1000E::IsInitialized()) { + KernelLogStream(WARNING, "Net") << "No NIC initialized, skipping network stack"; return; } @@ -36,8 +37,12 @@ namespace Net { Tcp::Initialize(); Socket::Initialize(); - // Hook E1000 RX to our Ethernet dispatcher - Drivers::Net::E1000::SetRxCallback(Ethernet::OnFrameReceived); + // Hook the active NIC's RX to our Ethernet dispatcher + if (Drivers::Net::E1000::IsInitialized()) { + Drivers::Net::E1000::SetRxCallback(Ethernet::OnFrameReceived); + } else { + Drivers::Net::E1000E::SetRxCallback(Ethernet::OnFrameReceived); + } // Send a gratuitous ARP to announce ourselves on the network Arp::SendRequest(GetIpAddress()); diff --git a/kernel/src/Net/Socket.cpp b/kernel/src/Net/Socket.cpp index efa4452..5949a85 100644 --- a/kernel/src/Net/Socket.cpp +++ b/kernel/src/Net/Socket.cpp @@ -1,19 +1,42 @@ /* * Socket.cpp - * Socket descriptor table wrapping kernel TCP layer + * Socket descriptor table wrapping kernel TCP and UDP layers * Copyright (c) 2025 Daniel Hammer */ #include "Socket.hpp" #include +#include #include #include #include +#include using namespace Kt; namespace Net::Socket { + // ---- UDP socket state ---- + + static constexpr uint32_t UDP_RING_SIZE = 4096; + static constexpr int MAX_UDP_SOCKETS = 16; + + struct UdpDgramHeader { + uint32_t SrcIp; + uint16_t SrcPort; + uint16_t DataLen; + }; + + struct UdpSocketState { + uint8_t Ring[UDP_RING_SIZE]; + uint32_t Head; + uint32_t Tail; + uint32_t Count; + uint16_t LocalPort; + bool Active; + }; + + static UdpSocketState g_udpSockets[MAX_UDP_SOCKETS] = {}; static SocketEntry g_sockets[MAX_SOCKETS] = {}; static uint16_t g_nextEphemeralPort = 49152; @@ -30,15 +53,78 @@ namespace Net::Socket { return true; } + static UdpSocketState* AllocUdpState() { + for (int i = 0; i < MAX_UDP_SOCKETS; i++) { + if (!g_udpSockets[i].Active) { + g_udpSockets[i].Active = true; + g_udpSockets[i].Head = 0; + g_udpSockets[i].Tail = 0; + g_udpSockets[i].Count = 0; + g_udpSockets[i].LocalPort = 0; + return &g_udpSockets[i]; + } + } + return nullptr; + } + + static void FreeUdpState(UdpSocketState* state) { + if (state) { + if (state->LocalPort != 0) { + Udp::Unbind(state->LocalPort); + } + state->Active = false; + } + } + + static void UdpSocketDispatcher(uint32_t srcIp, uint16_t srcPort, + uint16_t dstPort, + const uint8_t* data, uint16_t length) { + for (int i = 0; i < MAX_UDP_SOCKETS; i++) { + if (g_udpSockets[i].Active && g_udpSockets[i].LocalPort == dstPort) { + UdpSocketState* st = &g_udpSockets[i]; + uint32_t needed = sizeof(UdpDgramHeader) + length; + if (st->Count + needed > UDP_RING_SIZE) { + return; // drop if buffer full + } + + // Enqueue header + UdpDgramHeader hdr; + hdr.SrcIp = srcIp; + hdr.SrcPort = srcPort; + hdr.DataLen = length; + + const uint8_t* hdrBytes = (const uint8_t*)&hdr; + for (uint32_t j = 0; j < sizeof(UdpDgramHeader); j++) { + st->Ring[st->Tail] = hdrBytes[j]; + st->Tail = (st->Tail + 1) % UDP_RING_SIZE; + } + + // Enqueue payload + for (uint16_t j = 0; j < length; j++) { + st->Ring[st->Tail] = data[j]; + st->Tail = (st->Tail + 1) % UDP_RING_SIZE; + } + + st->Count += needed; + return; + } + } + } + + // ---- Public API ---- + void Initialize() { for (int i = 0; i < MAX_SOCKETS; i++) { g_sockets[i].Active = false; } + for (int i = 0; i < MAX_UDP_SOCKETS; i++) { + g_udpSockets[i].Active = false; + } KernelLogStream(OK, "Net") << "Socket table initialized"; } int Create(int type, int pid) { - if (type != SOCK_TCP) return -1; + if (type != SOCK_TCP && type != SOCK_UDP) return -1; for (int i = 0; i < MAX_SOCKETS; i++) { if (!g_sockets[i].Active) { @@ -46,7 +132,18 @@ namespace Net::Socket { g_sockets[i].Type = type; g_sockets[i].OwnerPid = pid; g_sockets[i].TcpConn = nullptr; + g_sockets[i].UdpState = nullptr; g_sockets[i].LocalPort = 0; + + if (type == SOCK_UDP) { + UdpSocketState* us = AllocUdpState(); + if (!us) { + g_sockets[i].Active = false; + return -1; + } + g_sockets[i].UdpState = us; + } + return i; } } @@ -55,6 +152,7 @@ namespace Net::Socket { int Connect(int fd, uint32_t ip, uint16_t port, int pid) { if (!ValidFd(fd, pid)) return -1; + if (g_sockets[fd].Type != SOCK_TCP) return -1; if (g_sockets[fd].TcpConn != nullptr) return -1; uint16_t srcPort = AllocEphemeralPort(); @@ -70,11 +168,20 @@ namespace Net::Socket { int Bind(int fd, uint16_t port, int pid) { if (!ValidFd(fd, pid)) return -1; g_sockets[fd].LocalPort = port; + + if (g_sockets[fd].Type == SOCK_UDP) { + UdpSocketState* us = g_sockets[fd].UdpState; + if (!us) return -1; + us->LocalPort = port; + if (!Udp::Bind(port, UdpSocketDispatcher)) return -1; + } + return 0; } int Listen(int fd, int pid) { if (!ValidFd(fd, pid)) return -1; + if (g_sockets[fd].Type != SOCK_TCP) return -1; if (g_sockets[fd].LocalPort == 0) return -1; if (g_sockets[fd].TcpConn != nullptr) return -1; @@ -87,6 +194,7 @@ namespace Net::Socket { int Accept(int fd, int pid) { if (!ValidFd(fd, pid)) return -1; + if (g_sockets[fd].Type != SOCK_TCP) return -1; if (g_sockets[fd].TcpConn == nullptr) return -1; Tcp::Connection* clientConn = Tcp::Accept(g_sockets[fd].TcpConn); @@ -99,6 +207,7 @@ namespace Net::Socket { g_sockets[i].Type = SOCK_TCP; g_sockets[i].OwnerPid = pid; g_sockets[i].TcpConn = clientConn; + g_sockets[i].UdpState = nullptr; g_sockets[i].LocalPort = g_sockets[fd].LocalPort; return i; } @@ -111,22 +220,92 @@ namespace Net::Socket { int Send(int fd, const uint8_t* data, uint32_t len, int pid) { if (!ValidFd(fd, pid)) return -1; + if (g_sockets[fd].Type != SOCK_TCP) return -1; if (g_sockets[fd].TcpConn == nullptr) return -1; return Tcp::Send(g_sockets[fd].TcpConn, data, (uint16_t)len); } int Recv(int fd, uint8_t* buf, uint32_t maxLen, int pid) { if (!ValidFd(fd, pid)) return -1; + if (g_sockets[fd].Type != SOCK_TCP) return -1; if (g_sockets[fd].TcpConn == nullptr) return -1; return Tcp::ReceiveNonBlocking(g_sockets[fd].TcpConn, buf, (uint16_t)maxLen); } + int SendTo(int fd, const uint8_t* data, uint32_t len, + uint32_t destIp, uint16_t destPort, int pid) { + if (!ValidFd(fd, pid)) return -1; + if (g_sockets[fd].Type != SOCK_UDP) return -1; + + UdpSocketState* us = g_sockets[fd].UdpState; + if (!us) return -1; + + // Auto-bind ephemeral port if not already bound + if (us->LocalPort == 0) { + uint16_t ep = AllocEphemeralPort(); + us->LocalPort = ep; + g_sockets[fd].LocalPort = ep; + if (!Udp::Bind(ep, UdpSocketDispatcher)) return -1; + } + + if (!Udp::Send(destIp, us->LocalPort, destPort, data, (uint16_t)len)) { + return -1; + } + return (int)len; + } + + int RecvFrom(int fd, uint8_t* buf, uint32_t maxLen, + uint32_t* srcIp, uint16_t* srcPort, int pid) { + if (!ValidFd(fd, pid)) return -1; + if (g_sockets[fd].Type != SOCK_UDP) return -1; + + UdpSocketState* us = g_sockets[fd].UdpState; + if (!us) return -1; + + if (us->Count < sizeof(UdpDgramHeader)) { + return -1; // no data available + } + + // Dequeue header + UdpDgramHeader hdr; + uint8_t* hdrBytes = (uint8_t*)&hdr; + for (uint32_t j = 0; j < sizeof(UdpDgramHeader); j++) { + hdrBytes[j] = us->Ring[us->Head]; + us->Head = (us->Head + 1) % UDP_RING_SIZE; + } + us->Count -= sizeof(UdpDgramHeader); + + // Dequeue payload + uint16_t copyLen = hdr.DataLen; + if (copyLen > maxLen) copyLen = (uint16_t)maxLen; + + for (uint16_t j = 0; j < copyLen; j++) { + buf[j] = us->Ring[us->Head]; + us->Head = (us->Head + 1) % UDP_RING_SIZE; + } + + // Skip remaining data if buffer was too small + for (uint16_t j = copyLen; j < hdr.DataLen; j++) { + us->Head = (us->Head + 1) % UDP_RING_SIZE; + } + us->Count -= hdr.DataLen; + + if (srcIp) *srcIp = hdr.SrcIp; + if (srcPort) *srcPort = hdr.SrcPort; + + return (int)copyLen; + } + void Close(int fd, int pid) { if (!ValidFd(fd, pid)) return; if (g_sockets[fd].TcpConn != nullptr) { Tcp::Close(g_sockets[fd].TcpConn); g_sockets[fd].TcpConn = nullptr; } + if (g_sockets[fd].UdpState != nullptr) { + FreeUdpState(g_sockets[fd].UdpState); + g_sockets[fd].UdpState = nullptr; + } g_sockets[fd].Active = false; } @@ -137,6 +316,10 @@ namespace Net::Socket { Tcp::Close(g_sockets[i].TcpConn); g_sockets[i].TcpConn = nullptr; } + if (g_sockets[i].UdpState != nullptr) { + FreeUdpState(g_sockets[i].UdpState); + g_sockets[i].UdpState = nullptr; + } g_sockets[i].Active = false; } } diff --git a/kernel/src/Net/Socket.hpp b/kernel/src/Net/Socket.hpp index f681bd6..b72364a 100644 --- a/kernel/src/Net/Socket.hpp +++ b/kernel/src/Net/Socket.hpp @@ -1,6 +1,6 @@ /* * Socket.hpp - * Socket descriptor table for userspace TCP access + * Socket descriptor table for userspace TCP/UDP access * Copyright (c) 2025 Daniel Hammer */ @@ -14,11 +14,14 @@ namespace Net::Socket { static constexpr int SOCK_UDP = 2; static constexpr int MAX_SOCKETS = 64; + struct UdpSocketState; + struct SocketEntry { bool Active; int Type; int OwnerPid; Tcp::Connection* TcpConn; + UdpSocketState* UdpState; uint16_t LocalPort; }; @@ -45,6 +48,14 @@ namespace Net::Socket { // Receive data from a connected socket. Returns bytes received, 0 on close, or -1. int Recv(int fd, uint8_t* buf, uint32_t maxLen, int pid); + // Send a UDP datagram to a specific destination. Returns bytes sent or -1. + int SendTo(int fd, const uint8_t* data, uint32_t len, + uint32_t destIp, uint16_t destPort, int pid); + + // Receive a UDP datagram, returning source info. Returns bytes received or -1. + int RecvFrom(int fd, uint8_t* buf, uint32_t maxLen, + uint32_t* srcIp, uint16_t* srcPort, int pid); + // Close a socket. void Close(int fd, int pid); diff --git a/kernel/src/Net/Udp.cpp b/kernel/src/Net/Udp.cpp index 1840317..4c1cbb2 100644 --- a/kernel/src/Net/Udp.cpp +++ b/kernel/src/Net/Udp.cpp @@ -61,7 +61,7 @@ namespace Net::Udp { // Dispatch to bound callback for (uint32_t i = 0; i < MAX_BINDINGS; i++) { if (g_bindings[i].Active && g_bindings[i].Port == dstPort) { - g_bindings[i].Callback(srcIp, srcPort, payload, payloadLen); + g_bindings[i].Callback(srcIp, srcPort, dstPort, payload, payloadLen); return; } } diff --git a/kernel/src/Net/Udp.hpp b/kernel/src/Net/Udp.hpp index 6090182..f6f5ae9 100644 --- a/kernel/src/Net/Udp.hpp +++ b/kernel/src/Net/Udp.hpp @@ -19,7 +19,9 @@ namespace Net::Udp { } __attribute__((packed)); // Callback type for receiving UDP data - using RecvCallback = void(*)(uint32_t srcIp, uint16_t srcPort, const uint8_t* data, uint16_t length); + using RecvCallback = void(*)(uint32_t srcIp, uint16_t srcPort, + uint16_t dstPort, + const uint8_t* data, uint16_t length); // Initialize the UDP subsystem void Initialize(); diff --git a/kernel/src/Pci/Pci.cpp b/kernel/src/Pci/Pci.cpp index a36e88b..63f683d 100644 --- a/kernel/src/Pci/Pci.cpp +++ b/kernel/src/Pci/Pci.cpp @@ -223,6 +223,32 @@ namespace Pci { return LegacyRead8(bus, device, function, (uint8_t)offset); } + // ------------------------------------------------------------------------- + // PCI capability list traversal + // ------------------------------------------------------------------------- + + uint8_t FindCapability(uint8_t bus, uint8_t device, uint8_t function, uint8_t capId) { + // Check Status register bit 4 (Capabilities List present) + uint16_t status = ReadConfig16(bus, device, function, RegStatus); + if (!(status & (1 << 4))) { + return 0; + } + + // Read Capabilities Pointer (offset 0x34), mask to dword-aligned + uint8_t offset = ReadConfig8(bus, device, function, 0x34) & 0xFC; + + // Walk the linked list (cap_id @ +0, next_ptr @ +1) + while (offset != 0) { + uint8_t id = ReadConfig8(bus, device, function, offset); + if (id == capId) { + return offset; + } + offset = ReadConfig8(bus, device, function, offset + 1) & 0xFC; + } + + return 0; + } + // ------------------------------------------------------------------------- // PCI class code names // ------------------------------------------------------------------------- diff --git a/kernel/src/Pci/Pci.hpp b/kernel/src/Pci/Pci.hpp index 878068d..93a317e 100644 --- a/kernel/src/Pci/Pci.hpp +++ b/kernel/src/Pci/Pci.hpp @@ -60,6 +60,13 @@ namespace Pci { void LegacyWrite16(uint8_t bus, uint8_t device, uint8_t function, uint8_t offset, uint16_t value); void LegacyWrite32(uint8_t bus, uint8_t device, uint8_t function, uint8_t offset, uint32_t value); + // PCI capability IDs + constexpr uint8_t PCI_CAP_MSI = 0x05; + + // Walk the PCI capability linked list for a given device. + // Returns the config-space offset of the capability, or 0 if not found. + uint8_t FindCapability(uint8_t bus, uint8_t device, uint8_t function, uint8_t capId); + // Class code name lookup const char* GetClassName(uint8_t classCode, uint8_t subClass); diff --git a/kernel/src/Sched/ElfLoader.cpp b/kernel/src/Sched/ElfLoader.cpp index d2e4d84..f942400 100644 --- a/kernel/src/Sched/ElfLoader.cpp +++ b/kernel/src/Sched/ElfLoader.cpp @@ -54,7 +54,6 @@ namespace Sched { uint64_t ElfLoad(const char* vfsPath, uint64_t pml4Phys) { int handle = Fs::Vfs::VfsOpen(vfsPath); if (handle < 0) { - Kt::KernelLogStream(Kt::ERROR, "ELF") << "Failed to open " << vfsPath; return 0; } diff --git a/kernel/src/Sched/Scheduler.cpp b/kernel/src/Sched/Scheduler.cpp index 6aeb8f9..865b58a 100644 --- a/kernel/src/Sched/Scheduler.cpp +++ b/kernel/src/Sched/Scheduler.cpp @@ -106,7 +106,6 @@ namespace Sched { // Load ELF into the process's address space uint64_t entry = ElfLoad(vfsPath, pml4Phys); if (entry == 0) { - Kt::KernelLogStream(Kt::ERROR, "Sched") << "Failed to load ELF: " << vfsPath; return -1; } @@ -204,6 +203,13 @@ namespace Sched { } void Schedule() { + // Reclaim terminated process slots so they can be reused + for (int i = 0; i < MaxProcesses; i++) { + if (processTable[i].state == ProcessState::Terminated) { + processTable[i].state = ProcessState::Free; + } + } + int next = -1; int start = (currentPid >= 0) ? currentPid + 1 : 0; @@ -311,9 +317,13 @@ namespace Sched { } bool IsAlive(int pid) { - if (pid < 0 || pid >= MaxProcesses) return false; - return processTable[pid].state == ProcessState::Ready - || processTable[pid].state == ProcessState::Running; + for (int i = 0; i < MaxProcesses; i++) { + if (processTable[i].pid == pid) { + return processTable[i].state == ProcessState::Ready + || processTable[i].state == ProcessState::Running; + } + } + return false; } } diff --git a/kernel/src/Timekeeping/ApicTimer.cpp b/kernel/src/Timekeeping/ApicTimer.cpp index 8883680..f08da4a 100644 --- a/kernel/src/Timekeeping/ApicTimer.cpp +++ b/kernel/src/Timekeeping/ApicTimer.cpp @@ -11,6 +11,7 @@ #include #include #include +#include using namespace Kt; @@ -37,10 +38,14 @@ namespace Timekeeping { static bool g_schedEnabled = false; - // Timer IRQ handler: increment tick count and drive scheduler + // Timer IRQ handler: increment tick count, poll NIC, and drive scheduler static void TimerHandler(uint8_t) { g_tickCount = g_tickCount + 1; + // In polling mode, drain the NIC's RX ring from timer context + // (equivalent to a real NIC IRQ handler, runs with interrupts disabled) + Drivers::Net::E1000E::Poll(); + if (g_schedEnabled) { Sched::Tick(); } diff --git a/programs/GNUmakefile b/programs/GNUmakefile index 93a810a..149cf99 100644 --- a/programs/GNUmakefile +++ b/programs/GNUmakefile @@ -56,29 +56,63 @@ BINDIR := bin # Discover all programs (each subdirectory under src/ is a program). PROGRAMS := $(notdir $(wildcard src/*)) -# Build targets: one ELF per program. -TARGETS := $(addprefix $(BINDIR)/,$(addsuffix .elf,$(PROGRAMS))) +# Games are built separately (doom has its own Makefile). +GAMES := doom +SYSTEM_PROGRAMS := $(filter-out $(GAMES),$(PROGRAMS)) + +# Build targets: system programs go to bin/os/, games are handled separately. +TARGETS := $(addprefix $(BINDIR)/os/,$(addsuffix .elf,$(SYSTEM_PROGRAMS))) + +# Game data files to copy into bin/games/. +GAME_DATA := $(BINDIR)/games/doom1.wad # Man pages source directory. MANDIR := man MANSRC := $(wildcard $(MANDIR)/*.*) MANDST := $(patsubst $(MANDIR)/%,$(BINDIR)/man/%,$(MANSRC)) -.PHONY: all clean +# Web content source directory. +WWWDIR := www +WWWSRC := $(wildcard $(WWWDIR)/*.*) +WWWDST := $(patsubst $(WWWDIR)/%,$(BINDIR)/www/%,$(WWWSRC)) -all: $(TARGETS) $(MANDST) +# Home directory placeholder. +HOMEKEEP := $(BINDIR)/home/.keep + +.PHONY: all clean doom + +all: $(TARGETS) doom $(GAME_DATA) $(MANDST) $(WWWDST) $(HOMEKEEP) + +# Build doom via its own Makefile. +doom: + $(MAKE) -C src/doom + +# Copy doom1.wad from data/games/ into bin/games/. +$(BINDIR)/games/doom1.wad: data/games/doom1.wad + mkdir -p $(BINDIR)/games + cp $< $@ # Copy man pages into bin/man/ so mkramdisk.sh picks them up. $(BINDIR)/man/%: $(MANDIR)/% mkdir -p $(BINDIR)/man cp $< $@ -# Build each program from its source files. -# For now each program is a single .cpp file compiled and linked directly. -$(BINDIR)/%.elf: src/%/main.cpp link.ld GNUmakefile - mkdir -p $(BINDIR) obj/$* +# Copy web content into bin/www/ so mkramdisk.sh picks them up. +$(BINDIR)/www/%: $(WWWDIR)/% + mkdir -p $(BINDIR)/www + cp $< $@ + +# Create empty home directory with a keep file. +$(HOMEKEEP): + mkdir -p $(BINDIR)/home + touch $@ + +# Build each system program from its source files. +$(BINDIR)/os/%.elf: src/%/main.cpp link.ld GNUmakefile + mkdir -p $(BINDIR)/os obj/$* $(CXX) $(CXXFLAGS) -c src/$*/main.cpp -o obj/$*/main.o $(CXX) $(CXXFLAGS) $(LDFLAGS) obj/$*/main.o -o $@ clean: rm -rf $(BINDIR) obj + $(MAKE) -C src/doom clean diff --git a/programs/bin/hello.elf b/programs/bin/hello.elf deleted file mode 100755 index 808e8e0..0000000 Binary files a/programs/bin/hello.elf and /dev/null differ diff --git a/programs/bin/shell.elf b/programs/bin/shell.elf deleted file mode 100755 index 4719627..0000000 Binary files a/programs/bin/shell.elf and /dev/null differ diff --git a/programs/data/games/doom1.wad b/programs/data/games/doom1.wad new file mode 100644 index 0000000..1a58f66 Binary files /dev/null and b/programs/data/games/doom1.wad differ diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index a56566f..e87d038 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -48,8 +48,23 @@ namespace Zenith { static constexpr uint64_t SYS_SEND = 34; static constexpr uint64_t SYS_RECV = 35; static constexpr uint64_t SYS_CLOSESOCK = 36; + static constexpr uint64_t SYS_GETNETCFG = 37; + static constexpr uint64_t SYS_SETNETCFG = 38; + static constexpr uint64_t SYS_SENDTO = 39; + static constexpr uint64_t SYS_RECVFROM = 40; + static constexpr uint64_t SYS_FWRITE = 41; + static constexpr uint64_t SYS_FCREATE = 42; static constexpr int SOCK_TCP = 1; + static constexpr int SOCK_UDP = 2; + + struct NetCfg { + uint32_t ipAddress; // network byte order + uint32_t subnetMask; // network byte order + uint32_t gateway; // network byte order + uint8_t macAddress[6]; + uint8_t _pad[2]; + }; struct DateTime { uint16_t Year; diff --git a/programs/include/zenith/string.h b/programs/include/zenith/string.h new file mode 100644 index 0000000..c89a9b7 --- /dev/null +++ b/programs/include/zenith/string.h @@ -0,0 +1,71 @@ +/* + * string.h + * Common string and memory utility functions for ZenithOS programs + * Copyright (c) 2025-2026 Daniel Hammer +*/ + +#pragma once +#include + +namespace zenith { + + inline int slen(const char* s) { + int n = 0; + while (s[n]) n++; + return n; + } + + inline bool streq(const char* a, const char* b) { + while (*a && *b) { + if (*a != *b) return false; + a++; b++; + } + return *a == *b; + } + + inline bool starts_with(const char* str, const char* prefix) { + while (*prefix) { + if (*str != *prefix) return false; + str++; prefix++; + } + return true; + } + + inline const char* skip_spaces(const char* s) { + while (*s == ' ') s++; + return s; + } + + inline void memcpy(void* dst, const void* src, uint64_t n) { + auto* d = (uint8_t*)dst; + auto* s = (const uint8_t*)src; + for (uint64_t i = 0; i < n; i++) d[i] = s[i]; + } + + inline void memmove(void* dst, const void* src, uint64_t n) { + auto* d = (uint8_t*)dst; + auto* s = (const uint8_t*)src; + if (d < s) { + for (uint64_t i = 0; i < n; i++) d[i] = s[i]; + } else { + for (uint64_t i = n; i > 0; i--) d[i-1] = s[i-1]; + } + } + + inline void memset(void* dst, int val, uint64_t n) { + auto* d = (uint8_t*)dst; + for (uint64_t i = 0; i < n; i++) d[i] = (uint8_t)val; + } + + inline void strcpy(char* dst, const char* src) { + while (*src) *dst++ = *src++; + *dst = '\0'; + } + + inline void strncpy(char* dst, const char* src, int max) { + int i = 0; + while (src[i] && i < max - 1) { dst[i] = src[i]; i++; } + dst[i] = '\0'; + } + +} diff --git a/programs/include/zenith/syscall.h b/programs/include/zenith/syscall.h index a457071..f770d22 100644 --- a/programs/include/zenith/syscall.h +++ b/programs/include/zenith/syscall.h @@ -136,6 +136,14 @@ namespace zenith { return (int)syscall3(Zenith::SYS_READDIR, (uint64_t)path, (uint64_t)names, (uint64_t)max); } + // File write/create + inline int fwrite(int handle, const uint8_t* buf, uint64_t off, uint64_t size) { + return (int)syscall4(Zenith::SYS_FWRITE, (uint64_t)handle, (uint64_t)buf, off, size); + } + inline int fcreate(const char* path) { + return (int)syscall1(Zenith::SYS_FCREATE, (uint64_t)path); + } + // Memory inline void* alloc(uint64_t size) { return (void*)syscall1(Zenith::SYS_ALLOC, size); } inline void free(void* ptr) { syscall1(Zenith::SYS_FREE, (uint64_t)ptr); } @@ -157,6 +165,10 @@ namespace zenith { return (int32_t)syscall2(Zenith::SYS_PING, (uint64_t)ip, (uint64_t)timeoutMs); } + // Network configuration + inline void get_netcfg(Zenith::NetCfg* out) { syscall1(Zenith::SYS_GETNETCFG, (uint64_t)out); } + inline int set_netcfg(const Zenith::NetCfg* cfg) { return (int)syscall1(Zenith::SYS_SETNETCFG, (uint64_t)cfg); } + // Sockets inline int socket(int type) { return (int)syscall1(Zenith::SYS_SOCKET, (uint64_t)type); @@ -182,6 +194,14 @@ namespace zenith { inline int closesocket(int fd) { return (int)syscall1(Zenith::SYS_CLOSESOCK, (uint64_t)fd); } + inline int sendto(int fd, const void* data, uint32_t len, uint32_t destIp, uint16_t destPort) { + return (int)syscall5(Zenith::SYS_SENDTO, (uint64_t)fd, (uint64_t)data, + (uint64_t)len, (uint64_t)destIp, (uint64_t)destPort); + } + inline int recvfrom(int fd, void* buf, uint32_t maxLen, uint32_t* srcIp, uint16_t* srcPort) { + return (int)syscall5(Zenith::SYS_RECVFROM, (uint64_t)fd, (uint64_t)buf, + (uint64_t)maxLen, (uint64_t)srcIp, (uint64_t)srcPort); + } // Process management inline void waitpid(int pid) { syscall1(Zenith::SYS_WAITPID, (uint64_t)pid); } diff --git a/programs/man/dhcp.1 b/programs/man/dhcp.1 new file mode 100644 index 0000000..41f6004 --- /dev/null +++ b/programs/man/dhcp.1 @@ -0,0 +1,48 @@ +.TH DHCP 1 +.SH NAME + dhcp - obtain network configuration via DHCP + +.SH SYNOPSIS + dhcp + +.SH DESCRIPTION + The DHCP client automatically obtains an IP address, subnet mask, + default gateway, and other network parameters from a DHCP server + on the local network using the Dynamic Host Configuration Protocol + (RFC 2131). + + On success the network configuration is applied immediately via + set_netcfg(). On failure the original configuration is restored. + + The client is run automatically by the init system at boot, but + may also be invoked manually from the shell. + +.SH PROTOCOL + The client performs the standard four-message DHCP exchange: + + 1. DHCPDISCOVER Broadcast to 255.255.255.255:67 + 2. DHCPOFFER Server offers an IP address + 3. DHCPREQUEST Client accepts the offered address + 4. DHCPACK Server confirms the lease + + The BROADCAST flag (0x8000) is set so that server replies are + sent to the broadcast address, since the client has no IP yet. + + Each step has a 10-second timeout. If no response is received + the client exits with an error and restores the previous config. + +.SH OUTPUT + On success the client prints the assigned configuration: + + IP Address, Subnet Mask, Gateway, DNS Server, Lease Time + +.SH OPTIONS + The DHCP client requests the following options from the server: + + 1 Subnet Mask + 3 Router (default gateway) + 6 DNS Server + 51 Lease Time + +.SH SEE ALSO + ifconfig(1), shell(1), syscalls(2) diff --git a/programs/man/edit.1 b/programs/man/edit.1 new file mode 100644 index 0000000..647aaac --- /dev/null +++ b/programs/man/edit.1 @@ -0,0 +1,51 @@ +.TH EDIT 1 +.SH NAME + edit - text editor for ZenithOS + +.SH SYNOPSIS + edit [filename] + +.SH DESCRIPTION + edit is an interactive text editor. When invoked with a filename, + it opens the file for editing. If the file does not exist, a new + empty buffer is created and will be saved to that path on write. + + When invoked without arguments, edit opens an empty buffer. You + will be prompted for a filename when saving. + +.SH KEYBOARD SHORTCUTS + +.SS Navigation + Arrow Keys Move cursor up/down/left/right + Home Move to start of line + End Move to end of line + Page Up Scroll up one page + Page Down Scroll down one page + +.SS Editing + Backspace Delete character before cursor + Delete Delete character at cursor + Enter Insert new line + Tab Insert 4 spaces + +.SS Commands + Ctrl+S Save file + Ctrl+Q Quit (warns if unsaved changes) + Ctrl+F Search for text + Ctrl+G Find next occurrence + +.SH DISPLAY + The top line shows the filename, a modified indicator [+], + and the current cursor position (Ln, Col). + + The bottom line shows keyboard shortcuts or status messages. + + Line numbers are displayed in a gutter on the left side. + Lines past the end of the file are marked with ~. + +.SH EXAMPLES + edit intro.1 Edit a file + edit Open a new empty buffer + +.SH SEE ALSO + cat(1), shell(1) diff --git a/programs/man/file.2 b/programs/man/file.2 index dc72f17..601cf3f 100644 --- a/programs/man/file.2 +++ b/programs/man/file.2 @@ -12,13 +12,13 @@ .SH DESCRIPTION ZenithOS provides a simple read-only Virtual File System (VFS) backed by the boot ramdisk. Files are accessed via paths in the - format ":/", where drive 0 is the ramdisk. + format ":/", where drive 0 is the ramdisk. .SS open Opens a file and returns a non-negative handle on success, or a negative value on error (file not found, no free handles). - int h = zenith::open("0:/shell.elf"); + int h = zenith::open("0:/os/hello.elf"); .SS read Reads up to 'size' bytes starting at 'offset' into 'buf'. @@ -42,15 +42,17 @@ .SS readdir Lists entries in a directory. Up to 'max' entry names (max 64) are written to the 'names' array. The kernel allocates a user- - accessible page for the string data automatically. + accessible page for the string data automatically. Directory + entries are returned with a trailing slash. const char* entries[64]; int count = zenith::readdir("0:/", entries, 64); + // entries: "os/", "games/", "man/", "www/", "home/" .SH READING PATTERN The standard pattern for reading a file: - int h = zenith::open("0:/myfile.txt"); + int h = zenith::open("0:/man/intro.1"); uint64_t size = zenith::getsize(h); uint8_t buf[512]; uint64_t off = 0; diff --git a/programs/man/init.1 b/programs/man/init.1 new file mode 100644 index 0000000..bd95fdb --- /dev/null +++ b/programs/man/init.1 @@ -0,0 +1,39 @@ +.TH INIT 1 +.SH NAME + init - ZenithOS init system + +.SH SYNOPSIS + Spawned automatically by the kernel as PID 0. + +.SH DESCRIPTION + init is the first userspace process started by the ZenithOS + kernel. It chains system services in sequence, then launches + the interactive shell. + + Each service is spawned as a child process. init waits for it + to exit before starting the next one. If a service fails to + spawn, init logs an error and continues to the next stage. + + Log output is timestamped and color-coded: + + HH:MM:SS INFO init Starting dhcp + HH:MM:SS OK init dhcp finished (pid 1) + +.SH BOOT SEQUENCE + The following services are started in order: + + 1. 0:/os/dhcp.elf Obtain network configuration via DHCP + 2. 0:/os/shell.elf Launch the interactive shell + + After the shell exits, init enters an idle loop. + +.SH LOG LEVELS + init uses four log levels, each with a distinct color: + + OK Green Service completed successfully + INFO Cyan Informational (service starting, etc.) + WARN Yellow Non-fatal warning + FAIL Red Service failed to start + +.SH SEE ALSO + dhcp(1), shell(1), syscalls(2) diff --git a/programs/man/intro.1 b/programs/man/intro.1 index 6c581cb..e7068b7 100644 --- a/programs/man/intro.1 +++ b/programs/man/intro.1 @@ -19,26 +19,42 @@ extern "C" void _start() { ... } - There is no argc/argv. Include for the full - typed syscall API. Include for malloc/mfree. + There is no argc/argv. Use zenith::getargs() to retrieve any + arguments passed by the parent process. Include + for the full typed syscall API. Build with: cd programs && make - The resulting ELF binary appears in programs/bin/. + The resulting ELF binary appears in programs/bin/os/. + +.SH RAMDISK LAYOUT + The boot ramdisk is mounted as drive 0 with the following + directory structure: + + 0:/os/ System binaries (shell, init, man, etc.) + 0:/games/ Games (doom) + 0:/man/ Manual pages + 0:/www/ Web server content + 0:/home/ User home directory .SH SHELL - The built-in shell is the primary way to interact with ZenithOS. - Type 'help' at the shell prompt for a list of commands. Use - 'man shell' for detailed shell documentation. + The interactive shell is the primary way to interact with + ZenithOS. Commands are resolved by searching the PATH + directories (0:/os/, 0:/games/) for matching .elf binaries. + Type 'help' at the shell prompt for a list of commands. + Use 'man shell' for detailed shell documentation. .SH MAN PAGES The following man pages are available: intro(1) This page shell(1) Shell commands reference + init(1) Init system + dhcp(1) DHCP client man(1) The man command itself + legal(7) Copyright and legal information syscalls(2) Overview of all syscalls spawn(2) Process spawning file(2) File I/O syscalls diff --git a/programs/man/man.1 b/programs/man/man.1 index ec65e0d..41ed5a3 100644 --- a/programs/man/man.1 +++ b/programs/man/man.1 @@ -11,7 +11,7 @@ fullscreen pager. Pages are stored as plain text files with simple formatting directives. - If no section is specified, sections 1, 2, and 3 are searched + If no section is specified, sections 1 through 7 are searched in order. If a section number is given, only that section is checked. @@ -27,9 +27,10 @@ q Quit .SH SECTIONS - 1 User commands (shell built-ins) + 1 User commands and programs 2 System calls (kernel interface) 3 Library functions (userspace libraries) + 7 Miscellaneous (legal, conventions) .SH FILES Man pages are stored on the ramdisk at: @@ -42,6 +43,7 @@ man intro View the introduction man 2 syscalls View syscall overview (section 2) man malloc View malloc documentation + man legal View copyright information .SH SEE ALSO intro(1), shell(1), syscalls(2) diff --git a/programs/man/shell.1 b/programs/man/shell.1 index 993076c..c7cc971 100644 --- a/programs/man/shell.1 +++ b/programs/man/shell.1 @@ -3,66 +3,101 @@ shell - ZenithOS interactive command shell .SH DESCRIPTION - The ZenithOS shell is a simple command interpreter that runs as - the first userspace process. It provides basic file inspection, - process management, networking, and documentation access. + The ZenithOS shell is a command interpreter launched by init + after system services have started. It provides command + execution, file navigation, and command history. -.SH COMMANDS + Commands are either shell builtins or external programs. When + a command is not a builtin, the shell searches for a matching + ELF binary and executes it as a child process. + +.SH COMMAND RESOLUTION + When a non-builtin command is entered, the shell searches for + a matching binary in the following order: + + 1. 0:/os/.elf + 2. 0:/games/.elf + 3. 0://.elf (if cwd is set) + 4. 0:/.elf + + The first match is spawned and the shell waits for it to exit. + If no match is found, the shell prints: + + : command not found + + Arguments after the command name are passed to the spawned + process. File path arguments are resolved against the current + working directory before being passed to external programs. + +.SH BUILTINS .SS help - Display a list of available commands. - -.SS info - Show the OS name, version, and syscall API version number. - -.SS man - Open a manual page in the fullscreen pager. See man(1). + Display a categorized list of available commands. .SS ls [dir] List files in the current directory, or in the specified directory. When a directory argument is given, the output shows only the entries inside that directory with the - directory prefix stripped. - Examples: ls, ls man + directory prefix stripped. Directory entries are shown + with a trailing slash. + Examples: ls, ls man, ls os .SS cd [dir] Change the working directory. With no argument or with /, returns to the root (0:/). Use cd .. to go up one level. + Trailing slashes on directory names are stripped. The shell prompt reflects the current directory. - Examples: cd man, cd .., cd - -.SS cat - Print the contents of a file to the terminal. The file - path is resolved relative to the current directory. - Example: cat hello.elf - -.SS run - Spawn a new process from an ELF binary and wait for it to - exit. The file path is resolved relative to the current - directory. The shell blocks until the child process terminates. - Example: run hello.elf - -.SS ping - Send 4 ICMP echo requests to the given IP address and display - round-trip times. Timeout is 3 seconds per request. - Example: ping 10.0.2.2 - -.SS date - Display the current date and time in UTC. - -.SS uptime - Display the system uptime in minutes, seconds, and milliseconds. - -.SS clear - Clear the terminal screen. + Examples: cd os, cd .., cd .SS exit Terminate the shell process. +.SH EXTERNAL COMMANDS + +.SS System commands (0:/os/) + man View manual pages + cat Display file contents + info Show system information + date Show current date and time + uptime Show system uptime + clear Clear the screen and framebuffer + reset Reboot the system + shutdown Shut down the system + +.SS Network commands (0:/os/) + ping Send ICMP echo requests + ifconfig Show/set network configuration + tcpconnect Interactive TCP client + irc IRC client + dhcp DHCP client + fetch HTTP client + httpd HTTP server + +.SS Games (0:/games/) + doom DOOM + .SH INPUT - The shell reads input character by character using SYS_GETCHAR. - Backspace is supported. Lines are limited to 255 characters. - There is no command history or tab completion. + The shell uses non-blocking keyboard input via SYS_GETKEY to + support arrow key detection. Lines are limited to 255 + characters. + +.SS Editing + Backspace Delete character before cursor + Enter Execute command + +.SS History + The shell stores the last 32 unique commands. Duplicate + consecutive entries are suppressed. + + Up Arrow Recall previous command + Down Arrow Recall next command (or clear line) + +.SH PROMPT + The prompt displays the current working directory: + + 0:/> _ (at root) + 0:/os> _ (in os/ directory) + 0:/man> _ (in man/ directory) .SH SEE ALSO man(1), intro(1), syscalls(2) diff --git a/programs/man/spawn.2 b/programs/man/spawn.2 index 2be6d18..d2982f3 100644 --- a/programs/man/spawn.2 +++ b/programs/man/spawn.2 @@ -13,11 +13,11 @@ Loads the ELF64 binary at the given VFS path and creates a new process. The path must include the drive prefix, for example: - int pid = zenith::spawn("0:/hello.elf"); + int pid = zenith::spawn("0:/os/hello.elf"); An optional second argument passes a string to the child: - int pid = zenith::spawn("0:/man.elf", "intro"); + int pid = zenith::spawn("0:/os/man.elf", "intro"); The new process gets its own PML4 page table, a 16 KiB stack (at 0x7FFFFEF000-0x7FFFFFF000), and begins executing at the @@ -40,7 +40,7 @@ .SH EXAMPLES Spawn a program and wait for it: - int pid = zenith::spawn("0:/hello.elf"); + int pid = zenith::spawn("0:/os/hello.elf"); if (pid < 0) { zenith::print("spawn failed\n"); } else { diff --git a/programs/man/syscalls.2 b/programs/man/syscalls.2 index 7a5b101..6d4d8eb 100644 --- a/programs/man/syscalls.2 +++ b/programs/man/syscalls.2 @@ -3,7 +3,7 @@ syscalls - overview of ZenithOS system calls .SH DESCRIPTION - ZenithOS provides 37 system calls (numbers 0-36) for userspace + ZenithOS provides 41 system calls (numbers 0-40) for userspace programs. Syscalls use the x86-64 SYSCALL instruction with the following register convention: @@ -120,8 +120,18 @@ Send an ICMP echo request and wait for reply. int32_t zenith::ping(uint32_t ip, uint32_t timeoutMs); +.B SYS_GETNETCFG (37) + Get the current network configuration (IP, mask, gateway, MAC). + void zenith::get_netcfg(Zenith::NetCfg* out); + +.B SYS_SETNETCFG (38) + Set the network configuration (IP, mask, gateway). + int zenith::set_netcfg(const Zenith::NetCfg* cfg); + +.SH SOCKETS .B SYS_SOCKET (29) - Create a socket. type=SOCK_TCP (1). Returns fd or -1. + Create a socket. type=SOCK_TCP (1) or SOCK_UDP (2). + Returns fd or -1. int zenith::socket(int type); .B SYS_CONNECT (30) @@ -147,13 +157,23 @@ .B SYS_RECV (35) Receive data from a connected socket. Returns bytes - received, 0 on connection close, or -1 on error. + received, 0 if no data available, or -1 on close/error. int zenith::recv(int fd, void* buf, uint32_t maxLen); .B SYS_CLOSESOCK (36) Close a socket and release its resources. int zenith::closesocket(int fd); +.B SYS_SENDTO (39) + Send a UDP datagram to a specific destination. + int zenith::sendto(int fd, const void* data, uint32_t len, + uint32_t destIp, uint16_t destPort); + +.B SYS_RECVFROM (40) + Receive a UDP datagram. Returns the source address. + int zenith::recvfrom(int fd, void* buf, uint32_t maxLen, + uint32_t* srcIp, uint16_t* srcPort); + .SH FRAMEBUFFER .B SYS_FBINFO (21) Get framebuffer dimensions and format. diff --git a/programs/obj/hello/main.o b/programs/obj/hello/main.o index ea63150..66a8d23 100644 Binary files a/programs/obj/hello/main.o and b/programs/obj/hello/main.o differ diff --git a/programs/obj/shell/main.o b/programs/obj/shell/main.o index 7c2be88..fad637a 100644 Binary files a/programs/obj/shell/main.o and b/programs/obj/shell/main.o differ diff --git a/programs/src/cat/main.cpp b/programs/src/cat/main.cpp new file mode 100644 index 0000000..2903895 --- /dev/null +++ b/programs/src/cat/main.cpp @@ -0,0 +1,66 @@ +/* + * main.cpp + * cat - Display file contents + * Copyright (c) 2025-2026 Daniel Hammer +*/ + +#include + +extern "C" void _start() { + char args[256]; + int len = zenith::getargs(args, sizeof(args)); + + if (len <= 0 || args[0] == '\0') { + zenith::print("Usage: cat \n"); + zenith::exit(1); + } + + // Build VFS path. If the path already starts with ":", use as-is. + // Otherwise, prefix "0:/" to it. + char path[256]; + bool hasPrefix = false; + if (args[0] >= '0' && args[0] <= '9' && args[1] == ':') { + hasPrefix = true; + } + + int i = 0; + if (!hasPrefix) { + path[0] = '0'; path[1] = ':'; path[2] = '/'; + i = 3; + } + int j = 0; + while (args[j] && i < 255) { + path[i++] = args[j++]; + } + path[i] = '\0'; + + int handle = zenith::open(path); + if (handle < 0) { + zenith::print("cat: cannot open '"); + zenith::print(args); + zenith::print("'\n"); + zenith::exit(1); + } + + uint64_t size = zenith::getsize(handle); + if (size == 0) { + zenith::close(handle); + zenith::exit(0); + } + + uint8_t buf[512]; + uint64_t offset = 0; + while (offset < size) { + uint64_t chunk = size - offset; + if (chunk > sizeof(buf) - 1) chunk = sizeof(buf) - 1; + int bytesRead = zenith::read(handle, buf, offset, chunk); + if (bytesRead <= 0) break; + buf[bytesRead] = '\0'; + zenith::print((const char*)buf); + offset += bytesRead; + } + + zenith::close(handle); + zenith::putchar('\n'); + zenith::exit(0); +} diff --git a/programs/src/clear/main.cpp b/programs/src/clear/main.cpp new file mode 100644 index 0000000..a6210bc --- /dev/null +++ b/programs/src/clear/main.cpp @@ -0,0 +1,27 @@ +/* + * main.cpp + * clear - Clear terminal screen and framebuffer + * Copyright (c) 2025-2026 Daniel Hammer +*/ + +#include + +extern "C" void _start() { + // Clear the raw framebuffer (needed after graphical programs like DOOM) + Zenith::FbInfo fb; + zenith::fb_info(&fb); + uint8_t* pixels = (uint8_t*)zenith::fb_map(); + if (pixels) { + for (uint64_t y = 0; y < fb.height; y++) { + uint32_t* row = (uint32_t*)(pixels + y * fb.pitch); + for (uint64_t x = 0; x < fb.width; x++) { + row[x] = 0x00000000; + } + } + } + + // Reset the text console + zenith::print("\033[2J"); // Clear entire screen + zenith::print("\033[H"); // Move cursor to top-left + zenith::exit(0); +} diff --git a/programs/src/date/main.cpp b/programs/src/date/main.cpp new file mode 100644 index 0000000..d284845 --- /dev/null +++ b/programs/src/date/main.cpp @@ -0,0 +1,56 @@ +/* + * main.cpp + * date - Show current date and time + * Copyright (c) 2025-2026 Daniel Hammer +*/ + +#include + +static void print_int(uint64_t n) { + if (n == 0) { + zenith::putchar('0'); + return; + } + char buf[20]; + int i = 0; + while (n > 0) { + buf[i++] = '0' + (n % 10); + n /= 10; + } + for (int j = i - 1; j >= 0; j--) { + zenith::putchar(buf[j]); + } +} + +static void print_int_padded(uint64_t n) { + if (n < 10) zenith::putchar('0'); + print_int(n); +} + +static const char* month_name(int m) { + static const char* months[] = { + "", "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" + }; + if (m >= 1 && m <= 12) return months[m]; + return "?"; +} + +extern "C" void _start() { + Zenith::DateTime dt; + zenith::gettime(&dt); + + print_int(dt.Day); + zenith::putchar(' '); + zenith::print(month_name(dt.Month)); + zenith::putchar(' '); + print_int(dt.Year); + zenith::print(", "); + print_int(dt.Hour); + zenith::putchar(':'); + print_int_padded(dt.Minute); + zenith::putchar(':'); + print_int_padded(dt.Second); + zenith::print(" UTC\n"); + zenith::exit(0); +} diff --git a/programs/src/dhcp/main.cpp b/programs/src/dhcp/main.cpp new file mode 100644 index 0000000..f1a721d --- /dev/null +++ b/programs/src/dhcp/main.cpp @@ -0,0 +1,495 @@ +/* + * main.cpp + * DHCP client for ZenithOS + * Obtains network configuration automatically via DHCP (RFC 2131) + * Copyright (c) 2025-2026 Daniel Hammer +*/ + +#include +#include + +using zenith::memcpy; +using zenith::memset; + +// ---- Minimal snprintf (no libc available) ---- + +using va_list = __builtin_va_list; +#define va_start __builtin_va_start +#define va_end __builtin_va_end +#define va_arg __builtin_va_arg + +struct PfState { + char* buf; + int pos; + int max; +}; + +static void pf_putc(PfState* st, char c) { + if (st->pos < st->max) st->buf[st->pos] = c; + st->pos++; +} + +static void pf_putnum(PfState* st, unsigned long val, int base, int width, char pad, int neg) { + char tmp[24]; + int i = 0; + const char* digits = "0123456789abcdef"; + if (val == 0) { tmp[i++] = '0'; } + else { while (val > 0) { tmp[i++] = digits[val % base]; val /= base; } } + int total = (neg ? 1 : 0) + i; + if (neg && pad == '0') pf_putc(st, '-'); + for (int w = total; w < width; w++) pf_putc(st, pad); + if (neg && pad != '0') pf_putc(st, '-'); + while (i > 0) pf_putc(st, tmp[--i]); +} + +static int vsnprintf(char* buf, int size, const char* fmt, va_list ap) { + PfState st; + st.buf = buf; + st.pos = 0; + st.max = size > 0 ? size - 1 : 0; + while (*fmt) { + if (*fmt != '%') { pf_putc(&st, *fmt++); continue; } + fmt++; + char pad = ' '; + if (*fmt == '0') { pad = '0'; fmt++; } + int width = 0; + while (*fmt >= '0' && *fmt <= '9') { width = width * 10 + (*fmt - '0'); fmt++; } + if (*fmt == 'l') fmt++; + switch (*fmt) { + case 'd': case 'i': { + long val = va_arg(ap, int); + int neg = 0; unsigned long uval; + if (val < 0) { neg = 1; uval = (unsigned long)(-val); } + else uval = (unsigned long)val; + pf_putnum(&st, uval, 10, width, pad, neg); + break; + } + case 'u': { unsigned val = va_arg(ap, unsigned); pf_putnum(&st, val, 10, width, pad, 0); break; } + case 'x': { unsigned val = va_arg(ap, unsigned); pf_putnum(&st, val, 16, width, pad, 0); break; } + case 's': { + const char* s = va_arg(ap, const char*); + if (!s) s = "(null)"; + int slen = 0; while (s[slen]) slen++; + for (int w = slen; w < width; w++) pf_putc(&st, ' '); + for (int j = 0; j < slen; j++) pf_putc(&st, s[j]); + break; + } + case 'c': { char c = (char)va_arg(ap, int); pf_putc(&st, c); break; } + case '%': pf_putc(&st, '%'); break; + default: pf_putc(&st, '%'); pf_putc(&st, *fmt); break; + } + if (*fmt) fmt++; + } + if (size > 0) { + if (st.pos < size) st.buf[st.pos] = '\0'; + else st.buf[size - 1] = '\0'; + } + return st.pos; +} + +static int snprintf(char* buf, int size, const char* fmt, ...) { + va_list ap; + va_start(ap, fmt); + int ret = vsnprintf(buf, size, fmt, ap); + va_end(ap); + return ret; +} + + + +// ---- DHCP constants ---- + +static constexpr uint8_t BOOTREQUEST = 1; +static constexpr uint8_t BOOTREPLY = 2; +static constexpr uint8_t HTYPE_ETH = 1; +static constexpr uint8_t HLEN_ETH = 6; + +static constexpr uint16_t DHCP_SERVER_PORT = 67; +static constexpr uint16_t DHCP_CLIENT_PORT = 68; + +static constexpr uint32_t DHCP_MAGIC = 0x63825363; // network byte order + +static constexpr uint16_t BROADCAST_FLAG = 0x8000; + +// DHCP message types +static constexpr uint8_t DHCPDISCOVER = 1; +static constexpr uint8_t DHCPOFFER = 2; +static constexpr uint8_t DHCPREQUEST = 3; +static constexpr uint8_t DHCPACK = 5; +static constexpr uint8_t DHCPNAK = 6; + +// DHCP option codes +static constexpr uint8_t OPT_SUBNET = 1; +static constexpr uint8_t OPT_ROUTER = 3; +static constexpr uint8_t OPT_DNS = 6; +static constexpr uint8_t OPT_REQUESTED_IP = 50; +static constexpr uint8_t OPT_LEASE_TIME = 51; +static constexpr uint8_t OPT_MSG_TYPE = 53; +static constexpr uint8_t OPT_SERVER_ID = 54; +static constexpr uint8_t OPT_PARAM_LIST = 55; +static constexpr uint8_t OPT_END = 255; + +// ---- DHCP packet structure ---- + +struct DhcpPacket { + uint8_t op; + uint8_t htype; + uint8_t hlen; + uint8_t hops; + uint32_t xid; + uint16_t secs; + uint16_t flags; + uint32_t ciaddr; + uint32_t yiaddr; + uint32_t siaddr; + uint32_t giaddr; + uint8_t chaddr[16]; + uint8_t sname[64]; + uint8_t file[128]; + uint8_t options[312]; +} __attribute__((packed)); + +// ---- Byte order helpers (DHCP uses network byte order) ---- + +static uint16_t htons(uint16_t v) { + return (uint16_t)((v >> 8) | (v << 8)); +} + +static uint32_t htonl(uint32_t v) { + return ((v >> 24) & 0xFF) | ((v >> 8) & 0xFF00) | + ((v << 8) & 0xFF0000) | ((v << 24) & 0xFF000000u); +} + +static uint32_t ntohl(uint32_t v) { return htonl(v); } + +// ---- IP formatting ---- + +static void format_ip(char* buf, int size, uint32_t ip) { + const uint8_t* b = (const uint8_t*)&ip; + snprintf(buf, size, "%d.%d.%d.%d", b[0], b[1], b[2], b[3]); +} + +static void format_mac(char* buf, int size, const uint8_t* mac) { + snprintf(buf, size, "%02x:%02x:%02x:%02x:%02x:%02x", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); +} + +// ---- Build DHCP packets ---- + +static uint32_t g_xid = 0x5A454E49; // "ZENI" + +static void build_base(DhcpPacket* pkt, const uint8_t* mac) { + memset(pkt, 0, sizeof(DhcpPacket)); + pkt->op = BOOTREQUEST; + pkt->htype = HTYPE_ETH; + pkt->hlen = HLEN_ETH; + pkt->xid = g_xid; + pkt->flags = htons(BROADCAST_FLAG); + memcpy(pkt->chaddr, mac, 6); + + // Magic cookie + pkt->options[0] = 0x63; + pkt->options[1] = 0x82; + pkt->options[2] = 0x53; + pkt->options[3] = 0x63; +} + +static int build_discover(DhcpPacket* pkt, const uint8_t* mac) { + build_base(pkt, mac); + + int off = 4; // after magic cookie + + // Option 53: DHCP Message Type = DISCOVER + pkt->options[off++] = OPT_MSG_TYPE; + pkt->options[off++] = 1; + pkt->options[off++] = DHCPDISCOVER; + + // Option 55: Parameter Request List + pkt->options[off++] = OPT_PARAM_LIST; + pkt->options[off++] = 4; + pkt->options[off++] = OPT_SUBNET; + pkt->options[off++] = OPT_ROUTER; + pkt->options[off++] = OPT_DNS; + pkt->options[off++] = OPT_LEASE_TIME; + + pkt->options[off++] = OPT_END; + + return (int)sizeof(DhcpPacket) - 312 + off; +} + +static int build_request(DhcpPacket* pkt, const uint8_t* mac, + uint32_t requestedIp, uint32_t serverId) { + build_base(pkt, mac); + + int off = 4; + + // Option 53: DHCP Message Type = REQUEST + pkt->options[off++] = OPT_MSG_TYPE; + pkt->options[off++] = 1; + pkt->options[off++] = DHCPREQUEST; + + // Option 50: Requested IP Address + pkt->options[off++] = OPT_REQUESTED_IP; + pkt->options[off++] = 4; + memcpy(&pkt->options[off], &requestedIp, 4); + off += 4; + + // Option 54: Server Identifier + pkt->options[off++] = OPT_SERVER_ID; + pkt->options[off++] = 4; + memcpy(&pkt->options[off], &serverId, 4); + off += 4; + + // Option 55: Parameter Request List + pkt->options[off++] = OPT_PARAM_LIST; + pkt->options[off++] = 4; + pkt->options[off++] = OPT_SUBNET; + pkt->options[off++] = OPT_ROUTER; + pkt->options[off++] = OPT_DNS; + pkt->options[off++] = OPT_LEASE_TIME; + + pkt->options[off++] = OPT_END; + + return (int)sizeof(DhcpPacket) - 312 + off; +} + +// ---- Parse DHCP options ---- + +struct DhcpOffer { + uint32_t offeredIp; + uint32_t serverId; + uint32_t subnetMask; + uint32_t router; + uint32_t dns; + uint32_t leaseTime; + uint8_t msgType; + bool valid; +}; + +static void parse_options(const DhcpPacket* pkt, DhcpOffer* offer) { + offer->offeredIp = pkt->yiaddr; + offer->serverId = 0; + offer->subnetMask = 0; + offer->router = 0; + offer->dns = 0; + offer->leaseTime = 0; + offer->msgType = 0; + offer->valid = false; + + // Verify magic cookie + if (pkt->options[0] != 0x63 || pkt->options[1] != 0x82 || + pkt->options[2] != 0x53 || pkt->options[3] != 0x63) { + return; + } + + int off = 4; + while (off < 312) { + uint8_t code = pkt->options[off++]; + if (code == OPT_END) break; + if (code == 0) continue; // pad + + if (off >= 312) break; + uint8_t len = pkt->options[off++]; + if (off + len > 312) break; + + switch (code) { + case OPT_MSG_TYPE: + if (len >= 1) offer->msgType = pkt->options[off]; + break; + case OPT_SUBNET: + if (len >= 4) memcpy(&offer->subnetMask, &pkt->options[off], 4); + break; + case OPT_ROUTER: + if (len >= 4) memcpy(&offer->router, &pkt->options[off], 4); + break; + case OPT_DNS: + if (len >= 4) memcpy(&offer->dns, &pkt->options[off], 4); + break; + case OPT_SERVER_ID: + if (len >= 4) memcpy(&offer->serverId, &pkt->options[off], 4); + break; + case OPT_LEASE_TIME: + if (len >= 4) { + uint32_t raw; + memcpy(&raw, &pkt->options[off], 4); + offer->leaseTime = ntohl(raw); + } + break; + } + + off += len; + } + + offer->valid = (offer->msgType != 0); +} + +// ---- Broadcast destination ---- + +static constexpr uint32_t BROADCAST_IP = 0xFFFFFFFF; + +// ---- Main ---- + +extern "C" void _start() { + char msg[256]; + + zenith::print("ZenithOS DHCP Client\n"); + + // 1. Get MAC address + Zenith::NetCfg origCfg; + zenith::get_netcfg(&origCfg); + + char macStr[32]; + format_mac(macStr, sizeof(macStr), origCfg.macAddress); + snprintf(msg, sizeof(msg), "MAC address: %s\n", macStr); + zenith::print(msg); + + // 2. Set IP to 0.0.0.0 to allow broadcast send/receive + Zenith::NetCfg zeroCfg; + zeroCfg.ipAddress = 0; + zeroCfg.subnetMask = 0; + zeroCfg.gateway = 0; + zenith::set_netcfg(&zeroCfg); + + // 3. Create UDP socket and bind to port 68 + int fd = zenith::socket(Zenith::SOCK_UDP); + if (fd < 0) { + zenith::print("Error: failed to create UDP socket\n"); + zenith::set_netcfg(&origCfg); + zenith::exit(1); + } + + if (zenith::bind(fd, DHCP_CLIENT_PORT) < 0) { + zenith::print("Error: failed to bind to port 68\n"); + zenith::closesocket(fd); + zenith::set_netcfg(&origCfg); + zenith::exit(1); + } + + // 4. Send DISCOVER + DhcpPacket pkt; + int pktLen = build_discover(&pkt, origCfg.macAddress); + + zenith::print("Sending DHCPDISCOVER...\n"); + if (zenith::sendto(fd, (const void*)&pkt, pktLen, BROADCAST_IP, DHCP_SERVER_PORT) < 0) { + zenith::print("Error: failed to send DISCOVER\n"); + zenith::closesocket(fd); + zenith::set_netcfg(&origCfg); + zenith::exit(1); + } + + // 5. Wait for OFFER + DhcpPacket resp; + DhcpOffer offer; + uint64_t startMs = zenith::get_milliseconds(); + bool gotOffer = false; + + zenith::print("Waiting for DHCPOFFER...\n"); + while (zenith::get_milliseconds() - startMs < 10000) { + uint32_t srcIp; + uint16_t srcPort; + int r = zenith::recvfrom(fd, (void*)&resp, sizeof(resp), &srcIp, &srcPort); + if (r > 0) { + if (resp.op == BOOTREPLY && resp.xid == g_xid) { + parse_options(&resp, &offer); + if (offer.valid && offer.msgType == DHCPOFFER) { + gotOffer = true; + break; + } + } + } + zenith::yield(); + } + + if (!gotOffer) { + zenith::print("Error: no DHCPOFFER received (timeout)\n"); + zenith::closesocket(fd); + zenith::set_netcfg(&origCfg); + zenith::exit(1); + } + + char ipStr[32]; + format_ip(ipStr, sizeof(ipStr), offer.offeredIp); + snprintf(msg, sizeof(msg), "Received OFFER: %s\n", ipStr); + zenith::print(msg); + + // 6. Send REQUEST + pktLen = build_request(&pkt, origCfg.macAddress, offer.offeredIp, offer.serverId); + + zenith::print("Sending DHCPREQUEST...\n"); + if (zenith::sendto(fd, (const void*)&pkt, pktLen, BROADCAST_IP, DHCP_SERVER_PORT) < 0) { + zenith::print("Error: failed to send REQUEST\n"); + zenith::closesocket(fd); + zenith::set_netcfg(&origCfg); + zenith::exit(1); + } + + // 7. Wait for ACK + bool gotAck = false; + startMs = zenith::get_milliseconds(); + + zenith::print("Waiting for DHCPACK...\n"); + while (zenith::get_milliseconds() - startMs < 10000) { + uint32_t srcIp; + uint16_t srcPort; + int r = zenith::recvfrom(fd, (void*)&resp, sizeof(resp), &srcIp, &srcPort); + if (r > 0) { + if (resp.op == BOOTREPLY && resp.xid == g_xid) { + parse_options(&resp, &offer); + if (offer.valid && offer.msgType == DHCPACK) { + gotAck = true; + break; + } + if (offer.valid && offer.msgType == DHCPNAK) { + zenith::print("Error: received DHCPNAK from server\n"); + zenith::closesocket(fd); + zenith::set_netcfg(&origCfg); + zenith::exit(1); + } + } + } + zenith::yield(); + } + + zenith::closesocket(fd); + + if (!gotAck) { + zenith::print("Error: no DHCPACK received (timeout)\n"); + zenith::set_netcfg(&origCfg); + zenith::exit(1); + } + + // 8. Apply configuration + Zenith::NetCfg newCfg; + newCfg.ipAddress = offer.offeredIp; + newCfg.subnetMask = offer.subnetMask; + newCfg.gateway = offer.router; + zenith::set_netcfg(&newCfg); + + // 9. Print results + zenith::print("\nDHCP configuration applied:\n"); + + format_ip(ipStr, sizeof(ipStr), offer.offeredIp); + snprintf(msg, sizeof(msg), " IP Address: %s\n", ipStr); + zenith::print(msg); + + format_ip(ipStr, sizeof(ipStr), offer.subnetMask); + snprintf(msg, sizeof(msg), " Subnet Mask: %s\n", ipStr); + zenith::print(msg); + + format_ip(ipStr, sizeof(ipStr), offer.router); + snprintf(msg, sizeof(msg), " Gateway: %s\n", ipStr); + zenith::print(msg); + + if (offer.dns != 0) { + format_ip(ipStr, sizeof(ipStr), offer.dns); + snprintf(msg, sizeof(msg), " DNS Server: %s\n", ipStr); + zenith::print(msg); + } + + if (offer.leaseTime != 0) { + snprintf(msg, sizeof(msg), " Lease Time: %u seconds\n", offer.leaseTime); + zenith::print(msg); + } + + zenith::exit(0); +} diff --git a/programs/src/doom/Makefile b/programs/src/doom/Makefile index c1485cd..4dd7207 100644 --- a/programs/src/doom/Makefile +++ b/programs/src/doom/Makefile @@ -163,14 +163,14 @@ ALL_OBJS := $(DOOM_OBJS) $(LOCAL_OBJS) # ---- Target ---- -TARGET := $(BINDIR)/doom.elf +TARGET := $(BINDIR)/games/doom.elf .PHONY: all clean all: $(TARGET) $(TARGET): $(ALL_OBJS) $(LINK_LD) Makefile - mkdir -p $(BINDIR) + mkdir -p $(BINDIR)/games $(LD) $(CFLAGS) $(LDFLAGS) $(ALL_OBJS) -o $@ # DOOM source files (from doomgeneric directory) diff --git a/programs/src/doom/doomgeneric_zenith.c b/programs/src/doom/doomgeneric_zenith.c index ee47db7..161c5b8 100644 --- a/programs/src/doom/doomgeneric_zenith.c +++ b/programs/src/doom/doomgeneric_zenith.c @@ -222,7 +222,7 @@ void DG_SetWindowTitle(const char* title) { /* ---- Entry point ---- */ void _start(void) { - char *argv[] = { "doom", "-iwad", "0:/doom1.wad", 0 }; + char *argv[] = { "doom", "-iwad", "0:/games/doom1.wad", 0 }; doomgeneric_Create(3, argv); for (;;) { doomgeneric_Tick(); diff --git a/programs/src/edit/main.cpp b/programs/src/edit/main.cpp new file mode 100644 index 0000000..35dcb23 --- /dev/null +++ b/programs/src/edit/main.cpp @@ -0,0 +1,832 @@ +/* + * main.cpp + * edit - Text editor for ZenithOS + * Copyright (c) 2025-2026 Daniel Hammer +*/ + +#include +#include +#include + +using zenith::slen; +using zenith::memcpy; +using zenith::memmove; + +// ---- Integer to string ---- + +static int itoa(int val, char* buf) { + if (val == 0) { buf[0] = '0'; buf[1] = '\0'; return 1; } + char tmp[12]; + int i = 0; + bool neg = false; + if (val < 0) { neg = true; val = -val; } + while (val > 0) { tmp[i++] = '0' + val % 10; val /= 10; } + int len = 0; + if (neg) buf[len++] = '-'; + for (int j = i - 1; j >= 0; j--) buf[len++] = tmp[j]; + buf[len] = '\0'; + return len; +} + +// ---- Terminal output helpers ---- + +static void print(const char* s) { zenith::print(s); } +static void putch(char c) { zenith::putchar(c); } + +static void print_int(int v) { + char buf[12]; + itoa(v, buf); + print(buf); +} + +// ANSI escape helpers +static void esc(const char* seq) { + putch('\033'); + putch('['); + print(seq); +} + +static void cursor_to(int row, int col) { + putch('\033'); putch('['); + print_int(row); putch(';'); + print_int(col); putch('H'); +} + +static void clear_line() { esc("2K"); } +static void hide_cursor() { esc("?25l"); } +static void show_cursor() { esc("?25h"); } +static void enter_alt_screen() { esc("?1049h"); } +static void exit_alt_screen() { esc("?1049l"); } +static void reset_attrs() { esc("0m"); } +static void reverse_video() { esc("7m"); } +static void dim_text() { esc("2m"); } + +// ---- Line buffer ---- + +struct Line { + char* data; + int len; + int cap; +}; + +static constexpr int MAX_LINES = 10000; +static constexpr int INITIAL_LINE_CAP = 64; +static constexpr int TAB_WIDTH = 4; + +static Line* lines = nullptr; +static int numLines = 0; + +// Editor state +static int cursorRow = 0; // cursor position in document (0-indexed) +static int cursorCol = 0; +static int topLine = 0; // first visible line +static int leftCol = 0; // horizontal scroll offset +static int screenRows = 24; +static int screenCols = 80; +static int editorRows = 0; // screenRows - 2 (status + hint bars) +static int gutterWidth = 4; // line number gutter width + +static bool modified = false; +static bool running = true; +static bool fullRedraw = true; + +static char filename[256] = ""; +static bool hasFilename = false; + +// Search state +static char searchQuery[128] = ""; +static int searchLen = 0; + +// Status message +static char statusMsg[128] = ""; +static uint64_t statusMsgTime = 0; + +// ---- Line operations ---- + +static void line_init(Line* ln) { + ln->cap = INITIAL_LINE_CAP; + ln->data = (char*)zenith::malloc(ln->cap); + ln->len = 0; + ln->data[0] = '\0'; +} + +static void line_ensure(Line* ln, int needed) { + if (needed + 1 <= ln->cap) return; + int newCap = ln->cap; + while (newCap < needed + 1) newCap *= 2; + ln->data = (char*)zenith::realloc(ln->data, newCap); + ln->cap = newCap; +} + +static void line_insert_char(Line* ln, int pos, char c) { + if (pos < 0) pos = 0; + if (pos > ln->len) pos = ln->len; + line_ensure(ln, ln->len + 1); + memmove(ln->data + pos + 1, ln->data + pos, ln->len - pos); + ln->data[pos] = c; + ln->len++; + ln->data[ln->len] = '\0'; +} + +static void line_delete_char(Line* ln, int pos) { + if (pos < 0 || pos >= ln->len) return; + memmove(ln->data + pos, ln->data + pos + 1, ln->len - pos - 1); + ln->len--; + ln->data[ln->len] = '\0'; +} + +static void line_append(Line* ln, const char* s, int slen) { + line_ensure(ln, ln->len + slen); + memcpy(ln->data + ln->len, s, slen); + ln->len += slen; + ln->data[ln->len] = '\0'; +} + +// ---- Document operations ---- + +static void insert_line(int at) { + if (numLines >= MAX_LINES) return; + if (at < 0) at = 0; + if (at > numLines) at = numLines; + + // Shift lines down + for (int i = numLines; i > at; i--) { + lines[i] = lines[i - 1]; + } + line_init(&lines[at]); + numLines++; +} + +static void delete_line(int at) { + if (at < 0 || at >= numLines) return; + if (numLines <= 1) { + // Don't delete the last line, just clear it + lines[at].len = 0; + lines[at].data[0] = '\0'; + return; + } + zenith::mfree(lines[at].data); + for (int i = at; i < numLines - 1; i++) { + lines[i] = lines[i + 1]; + } + numLines--; +} + +// ---- Compute gutter width from line count ---- + +static void update_gutter_width() { + int digits = 1; + int n = numLines; + while (n >= 10) { digits++; n /= 10; } + gutterWidth = digits + 2; // digits + space + separator + if (gutterWidth < 4) gutterWidth = 4; +} + +// ---- File I/O ---- + +static void set_status(const char* msg) { + int i = 0; + while (msg[i] && i < 126) { statusMsg[i] = msg[i]; i++; } + statusMsg[i] = '\0'; + statusMsgTime = zenith::get_milliseconds(); +} + +// Build VFS path from filename +static void build_path(const char* fname, char* out, int outMax) { + int i = 0; + // Check if already has drive prefix + bool hasPrefix = (fname[0] >= '0' && fname[0] <= '9' && fname[1] == ':'); + if (!hasPrefix) { + out[i++] = '0'; out[i++] = ':'; out[i++] = '/'; + } + int j = 0; + while (fname[j] && i < outMax - 1) { + out[i++] = fname[j++]; + } + out[i] = '\0'; +} + +static void load_file(const char* fname) { + char path[256]; + build_path(fname, path, sizeof(path)); + + int handle = zenith::open(path); + if (handle < 0) { + // New file + numLines = 1; + line_init(&lines[0]); + set_status("(New file)"); + return; + } + + uint64_t size = zenith::getsize(handle); + + // Read entire file into a temp buffer + uint8_t* buf = nullptr; + if (size > 0) { + buf = (uint8_t*)zenith::malloc(size + 1); + uint64_t off = 0; + while (off < size) { + int r = zenith::read(handle, buf + off, off, size - off); + if (r <= 0) break; + off += r; + } + buf[size] = '\0'; + } + zenith::close(handle); + + // Parse into lines + numLines = 0; + if (buf && size > 0) { + uint64_t lineStart = 0; + for (uint64_t i = 0; i <= size; i++) { + if (i == size || buf[i] == '\n') { + if (numLines >= MAX_LINES) break; + line_init(&lines[numLines]); + int lineLen = (int)(i - lineStart); + if (lineLen > 0) { + line_append(&lines[numLines], (const char*)buf + lineStart, lineLen); + } + numLines++; + lineStart = i + 1; + } + } + } + + if (buf) zenith::mfree(buf); + + if (numLines == 0) { + numLines = 1; + line_init(&lines[0]); + } + + set_status("File loaded"); +} + +static bool save_file() { + if (!hasFilename) { + set_status("No filename! Use Ctrl+S after setting a name"); + return false; + } + + char path[256]; + build_path(filename, path, sizeof(path)); + + // Calculate total size + uint64_t totalSize = 0; + for (int i = 0; i < numLines; i++) { + totalSize += lines[i].len; + if (i < numLines - 1) totalSize++; // newline + } + + // Try to open existing file first + int handle = zenith::open(path); + if (handle < 0) { + // Create new file + handle = zenith::fcreate(path); + if (handle < 0) { + set_status("Error: could not create file"); + return false; + } + } + + // Build content buffer and write + uint8_t* buf = (uint8_t*)zenith::malloc(totalSize + 1); + uint64_t off = 0; + for (int i = 0; i < numLines; i++) { + if (lines[i].len > 0) { + memcpy(buf + off, lines[i].data, lines[i].len); + off += lines[i].len; + } + if (i < numLines - 1) { + buf[off++] = '\n'; + } + } + + int result = zenith::fwrite(handle, buf, 0, totalSize); + zenith::close(handle); + zenith::mfree(buf); + + if (result < 0) { + set_status("Error: write failed"); + return false; + } + + modified = false; + set_status("File saved"); + return true; +} + +// ---- Prompt for input in the hint bar area ---- + +static int prompt_input(const char* promptStr, char* out, int outMax) { + // Draw prompt on the last row + cursor_to(screenRows, 1); + reverse_video(); + clear_line(); + print(promptStr); + reset_attrs(); + show_cursor(); + + int pos = 0; + out[0] = '\0'; + + while (true) { + if (!zenith::is_key_available()) { + zenith::yield(); + continue; + } + + Zenith::KeyEvent ev; + zenith::getkey(&ev); + if (!ev.pressed) continue; + + if (ev.ascii == '\033' || (ev.ctrl && ev.ascii == 'q')) { + // Cancel + return -1; + } + + if (ev.ascii == '\n') { + out[pos] = '\0'; + return pos; + } + + if (ev.ascii == '\b') { + if (pos > 0) { + pos--; + putch('\b'); putch(' '); putch('\b'); + } + } else if (ev.ascii >= ' ' && pos < outMax - 1) { + out[pos++] = ev.ascii; + out[pos] = '\0'; + putch(ev.ascii); + } + } +} + +// ---- Rendering ---- + +static void draw_status_bar() { + cursor_to(1, 1); + reverse_video(); + clear_line(); + + // Left side: "edit: filename [+]" + print(" edit: "); + if (hasFilename) { + print(filename); + } else { + print("[No Name]"); + } + if (modified) print(" [+]"); + + // Right side: cursor position + // Calculate right-side text + char posBuf[32]; + int p = 0; + posBuf[p++] = 'L'; + posBuf[p++] = 'n'; + posBuf[p++] = ' '; + p += itoa(cursorRow + 1, posBuf + p); + posBuf[p++] = ','; + posBuf[p++] = ' '; + posBuf[p++] = 'C'; + posBuf[p++] = 'o'; + posBuf[p++] = 'l'; + posBuf[p++] = ' '; + p += itoa(cursorCol + 1, posBuf + p); + posBuf[p++] = ' '; + posBuf[p++] = ' '; + posBuf[p] = '\0'; + + // Pad to right-align + // We need to figure out how many chars we've printed on the left + int leftLen = 8 + (hasFilename ? slen(filename) : 9) + (modified ? 4 : 0); + int rightLen = p; + int padding = screenCols - leftLen - rightLen; + for (int i = 0; i < padding; i++) putch(' '); + print(posBuf); + + reset_attrs(); +} + +static void draw_hint_bar() { + cursor_to(screenRows, 1); + reverse_video(); + clear_line(); + + // Show status message if recent (within 3 seconds) + uint64_t now = zenith::get_milliseconds(); + if (statusMsg[0] && (now - statusMsgTime) < 3000) { + print(" "); + print(statusMsg); + } else { + print(" ^S Save ^Q Quit ^F Find ^G Find Next"); + } + + // Pad rest of line + reset_attrs(); +} + +static void draw_line(int screenRow, int docLine) { + cursor_to(screenRow + 2, 1); // +2 because row 1 is status bar + clear_line(); + + if (docLine < numLines) { + // Line number gutter + dim_text(); + char numBuf[12]; + int numLen = itoa(docLine + 1, numBuf); + // Right-align the number in the gutter + int pad = gutterWidth - 2 - numLen; // -2 for trailing space+pipe + for (int i = 0; i < pad; i++) putch(' '); + print(numBuf); + putch(' '); + reset_attrs(); + + // Line content + Line* ln = &lines[docLine]; + int startCol = leftCol; + int maxChars = screenCols - gutterWidth; + + for (int c = 0; c < maxChars && startCol + c < ln->len; c++) { + putch(ln->data[startCol + c]); + } + } else { + // Past end of file + dim_text(); + putch('~'); + reset_attrs(); + } +} + +static void render() { + hide_cursor(); + + update_gutter_width(); + draw_status_bar(); + + if (fullRedraw) { + for (int i = 0; i < editorRows; i++) { + draw_line(i, topLine + i); + } + fullRedraw = false; + } + + draw_hint_bar(); + + // Position cursor + int screenY = cursorRow - topLine + 2; // +2 for status bar + int screenX = cursorCol - leftCol + gutterWidth; + cursor_to(screenY, screenX); + + show_cursor(); +} + +// ---- Scrolling ---- + +static void scroll() { + // Vertical scrolling + if (cursorRow < topLine) { + topLine = cursorRow; + fullRedraw = true; + } + if (cursorRow >= topLine + editorRows) { + topLine = cursorRow - editorRows + 1; + fullRedraw = true; + } + + // Horizontal scrolling + int textCols = screenCols - gutterWidth; + if (cursorCol < leftCol) { + leftCol = cursorCol; + fullRedraw = true; + } + if (cursorCol >= leftCol + textCols) { + leftCol = cursorCol - textCols + 1; + fullRedraw = true; + } +} + +// ---- Editing operations ---- + +static void insert_char(char c) { + line_insert_char(&lines[cursorRow], cursorCol, c); + cursorCol++; + modified = true; + fullRedraw = true; +} + +static void insert_tab() { + for (int i = 0; i < TAB_WIDTH; i++) { + insert_char(' '); + } +} + +static void insert_newline() { + Line* current = &lines[cursorRow]; + + // Split current line at cursor position + insert_line(cursorRow + 1); + // Re-get pointer since insert_line shifted memory + current = &lines[cursorRow]; + Line* newLine = &lines[cursorRow + 1]; + + // Move text after cursor to new line + if (cursorCol < current->len) { + line_append(newLine, current->data + cursorCol, current->len - cursorCol); + current->len = cursorCol; + current->data[current->len] = '\0'; + } + + cursorRow++; + cursorCol = 0; + modified = true; + fullRedraw = true; +} + +static void delete_char_backspace() { + if (cursorCol > 0) { + line_delete_char(&lines[cursorRow], cursorCol - 1); + cursorCol--; + modified = true; + fullRedraw = true; + } else if (cursorRow > 0) { + // Join with previous line + int prevLen = lines[cursorRow - 1].len; + line_append(&lines[cursorRow - 1], lines[cursorRow].data, lines[cursorRow].len); + delete_line(cursorRow); + cursorRow--; + cursorCol = prevLen; + modified = true; + fullRedraw = true; + } +} + +static void delete_char_forward() { + if (cursorCol < lines[cursorRow].len) { + line_delete_char(&lines[cursorRow], cursorCol); + modified = true; + fullRedraw = true; + } else if (cursorRow < numLines - 1) { + // Join with next line + line_append(&lines[cursorRow], lines[cursorRow + 1].data, lines[cursorRow + 1].len); + delete_line(cursorRow + 1); + modified = true; + fullRedraw = true; + } +} + +// ---- Search ---- + +static void find_next(bool fromPrompt) { + if (searchLen == 0) return; + + // Start search from cursor position + 1 + int startRow = cursorRow; + int startCol = cursorCol + (fromPrompt ? 0 : 1); + + for (int i = 0; i < numLines; i++) { + int row = (startRow + i) % numLines; + int colStart = (i == 0) ? startCol : 0; + + Line* ln = &lines[row]; + for (int c = colStart; c <= ln->len - searchLen; c++) { + bool match = true; + for (int k = 0; k < searchLen; k++) { + if (ln->data[c + k] != searchQuery[k]) { + match = false; + break; + } + } + if (match) { + cursorRow = row; + cursorCol = c; + fullRedraw = true; + set_status("Found"); + return; + } + } + } + + set_status("Not found"); +} + +static void do_search() { + char query[128]; + int len = prompt_input("Search: ", query, sizeof(query)); + if (len < 0) { + fullRedraw = true; + return; + } + if (len == 0) { + fullRedraw = true; + return; + } + + // Copy query + for (int i = 0; i < len; i++) searchQuery[i] = query[i]; + searchQuery[len] = '\0'; + searchLen = len; + + find_next(true); + fullRedraw = true; +} + +// ---- Navigation scancodes ---- + +static constexpr uint8_t SC_UP = 0x48; +static constexpr uint8_t SC_DOWN = 0x50; +static constexpr uint8_t SC_LEFT = 0x4B; +static constexpr uint8_t SC_RIGHT = 0x4D; +static constexpr uint8_t SC_HOME = 0x47; +static constexpr uint8_t SC_END = 0x4F; +static constexpr uint8_t SC_PGUP = 0x49; +static constexpr uint8_t SC_PGDN = 0x51; +static constexpr uint8_t SC_DELETE = 0x53; + +// ---- Input handling ---- + +static void handle_key(const Zenith::KeyEvent& ev) { + if (!ev.pressed) return; + + // Ctrl key combinations + if (ev.ctrl) { + switch (ev.ascii) { + case 'q': + if (modified) { + set_status("Unsaved changes! Press Ctrl+Q again to quit"); + // Set a flag so next Ctrl+Q quits + static bool warnedOnce = false; + if (warnedOnce) { + running = false; + } + warnedOnce = true; + return; + } + running = false; + return; + case 's': + if (!hasFilename) { + char nameBuf[256]; + int len = prompt_input("Save as: ", nameBuf, sizeof(nameBuf)); + if (len > 0) { + for (int i = 0; i <= len; i++) filename[i] = nameBuf[i]; + hasFilename = true; + } + fullRedraw = true; + if (!hasFilename) return; + } + save_file(); + fullRedraw = true; + return; + case 'f': + do_search(); + return; + case 'g': + find_next(false); + return; + default: + break; + } + } + + // Non-ASCII keys (scancode-based) + if (ev.ascii == 0) { + switch (ev.scancode) { + case SC_UP: + if (cursorRow > 0) { + cursorRow--; + if (cursorCol > lines[cursorRow].len) + cursorCol = lines[cursorRow].len; + fullRedraw = true; + } + return; + case SC_DOWN: + if (cursorRow < numLines - 1) { + cursorRow++; + if (cursorCol > lines[cursorRow].len) + cursorCol = lines[cursorRow].len; + fullRedraw = true; + } + return; + case SC_LEFT: + if (cursorCol > 0) { + cursorCol--; + } else if (cursorRow > 0) { + cursorRow--; + cursorCol = lines[cursorRow].len; + } + fullRedraw = true; + return; + case SC_RIGHT: + if (cursorCol < lines[cursorRow].len) { + cursorCol++; + } else if (cursorRow < numLines - 1) { + cursorRow++; + cursorCol = 0; + } + fullRedraw = true; + return; + case SC_HOME: + cursorCol = 0; + fullRedraw = true; + return; + case SC_END: + cursorCol = lines[cursorRow].len; + fullRedraw = true; + return; + case SC_PGUP: + cursorRow -= editorRows; + if (cursorRow < 0) cursorRow = 0; + if (cursorCol > lines[cursorRow].len) + cursorCol = lines[cursorRow].len; + fullRedraw = true; + return; + case SC_PGDN: + cursorRow += editorRows; + if (cursorRow >= numLines) cursorRow = numLines - 1; + if (cursorCol > lines[cursorRow].len) + cursorCol = lines[cursorRow].len; + fullRedraw = true; + return; + case SC_DELETE: + delete_char_forward(); + return; + default: + return; + } + } + + // Regular keys + switch (ev.ascii) { + case '\n': + insert_newline(); + break; + case '\b': + delete_char_backspace(); + break; + case '\t': + insert_tab(); + break; + default: + if (ev.ascii >= ' ') { + insert_char(ev.ascii); + } + break; + } +} + +// ---- Entry point ---- + +extern "C" void _start() { + // Allocate line buffer + lines = (Line*)zenith::malloc(sizeof(Line) * MAX_LINES); + + // Get terminal size + zenith::termsize(&screenCols, &screenRows); + editorRows = screenRows - 2; + + // Parse arguments + char args[256]; + int argLen = zenith::getargs(args, sizeof(args)); + + if (argLen > 0 && args[0] != '\0') { + // Copy filename + int i = 0; + while (args[i] && i < 255) { filename[i] = args[i]; i++; } + filename[i] = '\0'; + hasFilename = true; + load_file(filename); + } else { + // New empty buffer + numLines = 1; + line_init(&lines[0]); + } + + // Enter alternate screen + enter_alt_screen(); + fullRedraw = true; + + // Main loop + while (running) { + scroll(); + render(); + + // Wait for input + while (!zenith::is_key_available()) { + zenith::yield(); + } + + Zenith::KeyEvent ev; + zenith::getkey(&ev); + handle_key(ev); + } + + // Exit alternate screen + exit_alt_screen(); + show_cursor(); + reset_attrs(); + + zenith::exit(0); +} diff --git a/programs/src/fetch/main.cpp b/programs/src/fetch/main.cpp new file mode 100644 index 0000000..3d2e9a1 --- /dev/null +++ b/programs/src/fetch/main.cpp @@ -0,0 +1,359 @@ +/* + * main.cpp + * HTTP/1.0 client for ZenithOS + * Usage: run fetch.elf + * Copyright (c) 2025-2026 Daniel Hammer +*/ + +#include +#include + +using zenith::skip_spaces; +using zenith::memcpy; + +// ---- Minimal snprintf (no libc available) ---- + +using va_list = __builtin_va_list; +#define va_start __builtin_va_start +#define va_end __builtin_va_end +#define va_arg __builtin_va_arg + +struct PfState { + char* buf; + int pos; + int max; +}; + +static void pf_putc(PfState* st, char c) { + if (st->pos < st->max) st->buf[st->pos] = c; + st->pos++; +} + +static void pf_putnum(PfState* st, unsigned long val, int base, int width, char pad, int neg) { + char tmp[24]; + int i = 0; + const char* digits = "0123456789abcdef"; + if (val == 0) { tmp[i++] = '0'; } + else { while (val > 0) { tmp[i++] = digits[val % base]; val /= base; } } + int total = (neg ? 1 : 0) + i; + if (neg && pad == '0') pf_putc(st, '-'); + for (int w = total; w < width; w++) pf_putc(st, pad); + if (neg && pad != '0') pf_putc(st, '-'); + while (i > 0) pf_putc(st, tmp[--i]); +} + +static int vsnprintf(char* buf, int size, const char* fmt, va_list ap) { + PfState st; + st.buf = buf; + st.pos = 0; + st.max = size > 0 ? size - 1 : 0; + while (*fmt) { + if (*fmt != '%') { pf_putc(&st, *fmt++); continue; } + fmt++; + char pad = ' '; + if (*fmt == '0') { pad = '0'; fmt++; } + int width = 0; + while (*fmt >= '0' && *fmt <= '9') { width = width * 10 + (*fmt - '0'); fmt++; } + if (*fmt == 'l') fmt++; + switch (*fmt) { + case 'd': case 'i': { + long val = va_arg(ap, int); + int neg = 0; unsigned long uval; + if (val < 0) { neg = 1; uval = (unsigned long)(-val); } + else uval = (unsigned long)val; + pf_putnum(&st, uval, 10, width, pad, neg); + break; + } + case 'u': { unsigned val = va_arg(ap, unsigned); pf_putnum(&st, val, 10, width, pad, 0); break; } + case 'x': { unsigned val = va_arg(ap, unsigned); pf_putnum(&st, val, 16, width, pad, 0); break; } + case 's': { + const char* s = va_arg(ap, const char*); + if (!s) s = "(null)"; + int slen = 0; while (s[slen]) slen++; + for (int w = slen; w < width; w++) pf_putc(&st, ' '); + for (int j = 0; j < slen; j++) pf_putc(&st, s[j]); + break; + } + case 'c': { char c = (char)va_arg(ap, int); pf_putc(&st, c); break; } + case '%': pf_putc(&st, '%'); break; + default: pf_putc(&st, '%'); pf_putc(&st, *fmt); break; + } + if (*fmt) fmt++; + } + if (size > 0) { + if (st.pos < size) st.buf[st.pos] = '\0'; + else st.buf[size - 1] = '\0'; + } + return st.pos; +} + +static int snprintf(char* buf, int size, const char* fmt, ...) { + va_list ap; + va_start(ap, fmt); + int ret = vsnprintf(buf, size, fmt, ap); + va_end(ap); + return ret; +} + +// ---- IP/port parsing ---- + +static bool parse_ip(const char* s, uint32_t* out) { + uint32_t octets[4]; + int idx = 0; + uint32_t val = 0; + bool hasDigit = false; + for (int i = 0; ; i++) { + char c = s[i]; + if (c >= '0' && c <= '9') { + val = val * 10 + (c - '0'); + if (val > 255) return false; + hasDigit = true; + } else if (c == '.' || c == '\0') { + if (!hasDigit || idx >= 4) return false; + octets[idx++] = val; + val = 0; hasDigit = false; + if (c == '\0') break; + } else return false; + } + if (idx != 4) return false; + *out = octets[0] | (octets[1] << 8) | (octets[2] << 16) | (octets[3] << 24); + return true; +} + +static bool parse_uint16(const char* s, uint16_t* out) { + uint32_t val = 0; + if (*s == '\0') return false; + while (*s) { + if (*s < '0' || *s > '9') return false; + val = val * 10 + (*s - '0'); + if (val > 65535) return false; + s++; + } + *out = (uint16_t)val; + return true; +} + +// ---- HTTP response parser ---- + +// Find "\r\n\r\n" boundary between headers and body +static int find_header_end(const char* buf, int len) { + for (int i = 0; i + 3 < len; i++) { + if (buf[i] == '\r' && buf[i+1] == '\n' && buf[i+2] == '\r' && buf[i+3] == '\n') + return i + 4; + } + return -1; +} + +// Extract HTTP status code from first line: "HTTP/1.x NNN ..." +static int parse_status_code(const char* buf, int len) { + // Find first space + int i = 0; + while (i < len && buf[i] != ' ') i++; + if (i >= len) return -1; + i++; // skip space + // Parse 3-digit status + if (i + 2 >= len) return -1; + if (buf[i] < '0' || buf[i] > '9') return -1; + int code = (buf[i] - '0') * 100 + (buf[i+1] - '0') * 10 + (buf[i+2] - '0'); + return code; +} + +// Extract status text from first line: "HTTP/1.x NNN Status Text\r\n" +static void parse_status_text(const char* buf, int len, char* out, int outMax) { + // Skip "HTTP/1.x NNN " + int i = 0; + while (i < len && buf[i] != ' ') i++; + i++; // skip first space (after HTTP/1.x) + while (i < len && buf[i] != ' ') i++; + i++; // skip second space (after status code) + // Copy until \r or end + int j = 0; + while (i < len && buf[i] != '\r' && buf[i] != '\n' && j < outMax - 1) { + out[j++] = buf[i++]; + } + out[j] = '\0'; +} + +// ---- Main ---- + +extern "C" void _start() { + // Parse arguments: + char argbuf[512]; + zenith::getargs(argbuf, sizeof(argbuf)); + const char* arg = skip_spaces(argbuf); + + if (*arg == '\0') { + zenith::print("Usage: fetch.elf \n"); + zenith::print("Example: run fetch.elf 10.0.68.1 80 /\n"); + zenith::print(" run fetch.elf 93.184.216.34 80 /index.html\n"); + zenith::exit(0); + } + + // Parse IP + char ipStr[32]; + int i = 0; + while (arg[i] && arg[i] != ' ' && i < 31) { ipStr[i] = arg[i]; i++; } + ipStr[i] = '\0'; + arg = skip_spaces(arg + i); + + uint32_t serverIp; + if (!parse_ip(ipStr, &serverIp)) { + zenith::print("Invalid IP address: "); + zenith::print(ipStr); + zenith::putchar('\n'); + zenith::exit(1); + } + + // Parse port + char portStr[16]; + i = 0; + while (arg[i] && arg[i] != ' ' && i < 15) { portStr[i] = arg[i]; i++; } + portStr[i] = '\0'; + arg = skip_spaces(arg + i); + + uint16_t serverPort; + if (!parse_uint16(portStr, &serverPort)) { + zenith::print("Invalid port: "); + zenith::print(portStr); + zenith::putchar('\n'); + zenith::exit(1); + } + + // Parse path (rest of args, default to "/" if empty) + char path[256]; + if (*arg) { + i = 0; + while (arg[i] && i < 255) { path[i] = arg[i]; i++; } + path[i] = '\0'; + } else { + path[0] = '/'; path[1] = '\0'; + } + + // Print connection info + char msg[256]; + snprintf(msg, sizeof(msg), "Connecting to %s:%d...\n", ipStr, (int)serverPort); + zenith::print(msg); + + // Create socket + int fd = zenith::socket(Zenith::SOCK_TCP); + if (fd < 0) { + zenith::print("Error: failed to create socket\n"); + zenith::exit(1); + } + + // Connect + if (zenith::connect(fd, serverIp, serverPort) < 0) { + zenith::print("Error: connection failed\n"); + zenith::closesocket(fd); + zenith::exit(1); + } + + // Build and send HTTP request + char request[512]; + int reqLen = snprintf(request, sizeof(request), + "GET %s HTTP/1.0\r\n" + "Host: %s\r\n" + "User-Agent: ZenithOS/1.0\r\n" + "Connection: close\r\n" + "\r\n", + path, ipStr); + + snprintf(msg, sizeof(msg), "GET %s\n", path); + zenith::print(msg); + + if (zenith::send(fd, request, reqLen) < 0) { + zenith::print("Error: failed to send request\n"); + zenith::closesocket(fd); + zenith::exit(1); + } + + // Receive response + // We accumulate the full response to parse headers, then print the body. + // Use a large static buffer (32 KB) since we can't save to files anyway. + static char respBuf[32768]; + int respLen = 0; + bool aborted = false; + int idleCount = 0; + + while (respLen < (int)sizeof(respBuf) - 1) { + // Check for Ctrl+Q to abort + if (zenith::is_key_available()) { + Zenith::KeyEvent ev; + zenith::getkey(&ev); + if (ev.pressed && ev.ctrl && ev.ascii == 'q') { + aborted = true; + break; + } + } + + int r = zenith::recv(fd, respBuf + respLen, sizeof(respBuf) - 1 - respLen); + if (r > 0) { + respLen += r; + idleCount = 0; + } else if (r == 0) { + // Connection closed by server — done + break; + } else { + // No data available or error + idleCount++; + if (idleCount > 2000) { + // Assume connection closed after extended idle + break; + } + zenith::yield(); + } + } + + respBuf[respLen] = '\0'; + zenith::closesocket(fd); + + if (aborted) { + zenith::print("\nAborted.\n"); + zenith::exit(0); + } + + if (respLen == 0) { + zenith::print("Error: empty response\n"); + zenith::exit(1); + } + + // Parse response headers + int headerEnd = find_header_end(respBuf, respLen); + if (headerEnd < 0) { + // No proper header/body separator — just print everything + zenith::print("Warning: malformed response (no header boundary)\n\n"); + zenith::print(respBuf); + zenith::putchar('\n'); + zenith::exit(0); + } + + int statusCode = parse_status_code(respBuf, headerEnd); + char statusText[64]; + parse_status_text(respBuf, headerEnd, statusText, sizeof(statusText)); + + int bodyLen = respLen - headerEnd; + + // Print summary + snprintf(msg, sizeof(msg), "HTTP/1.0 %d %s (%d bytes)\n\n", statusCode, statusText, bodyLen); + zenith::print(msg); + + // Print body — it may contain null bytes in binary content, but for text we + // can just print as a string. For binary, we print what we can. + if (bodyLen > 0) { + // Print body in chunks (print expects null-terminated) + const char* body = respBuf + headerEnd; + int printed = 0; + char chunk[512]; + while (printed < bodyLen) { + int n = bodyLen - printed; + if (n > (int)sizeof(chunk) - 1) n = (int)sizeof(chunk) - 1; + memcpy(chunk, body + printed, n); + chunk[n] = '\0'; + zenith::print(chunk); + printed += n; + } + zenith::putchar('\n'); + } + + zenith::exit(0); +} diff --git a/programs/src/httpd/main.cpp b/programs/src/httpd/main.cpp new file mode 100644 index 0000000..e06562b --- /dev/null +++ b/programs/src/httpd/main.cpp @@ -0,0 +1,562 @@ +/* + * main.cpp + * HTTP/1.0 server for ZenithOS + * Usage: run httpd.elf [port] (default: 80) + * Serves a built-in index page and files from the VFS + * Copyright (c) 2025-2026 Daniel Hammer +*/ + +#include +#include + +using zenith::slen; +using zenith::streq; +using zenith::starts_with; +using zenith::skip_spaces; + +// ---- Minimal snprintf (no libc available) ---- + +using va_list = __builtin_va_list; +#define va_start __builtin_va_start +#define va_end __builtin_va_end +#define va_arg __builtin_va_arg + +struct PfState { + char* buf; + int pos; + int max; +}; + +static void pf_putc(PfState* st, char c) { + if (st->pos < st->max) st->buf[st->pos] = c; + st->pos++; +} + +static void pf_putnum(PfState* st, unsigned long val, int base, int width, char pad, int neg) { + char tmp[24]; + int i = 0; + const char* digits = "0123456789abcdef"; + if (val == 0) { tmp[i++] = '0'; } + else { while (val > 0) { tmp[i++] = digits[val % base]; val /= base; } } + int total = (neg ? 1 : 0) + i; + if (neg && pad == '0') pf_putc(st, '-'); + for (int w = total; w < width; w++) pf_putc(st, pad); + if (neg && pad != '0') pf_putc(st, '-'); + while (i > 0) pf_putc(st, tmp[--i]); +} + +static int vsnprintf(char* buf, int size, const char* fmt, va_list ap) { + PfState st; + st.buf = buf; + st.pos = 0; + st.max = size > 0 ? size - 1 : 0; + while (*fmt) { + if (*fmt != '%') { pf_putc(&st, *fmt++); continue; } + fmt++; + char pad = ' '; + if (*fmt == '0') { pad = '0'; fmt++; } + int width = 0; + while (*fmt >= '0' && *fmt <= '9') { width = width * 10 + (*fmt - '0'); fmt++; } + if (*fmt == 'l') fmt++; + switch (*fmt) { + case 'd': case 'i': { + long val = va_arg(ap, int); + int neg = 0; unsigned long uval; + if (val < 0) { neg = 1; uval = (unsigned long)(-val); } + else uval = (unsigned long)val; + pf_putnum(&st, uval, 10, width, pad, neg); + break; + } + case 'u': { unsigned val = va_arg(ap, unsigned); pf_putnum(&st, val, 10, width, pad, 0); break; } + case 'x': { unsigned val = va_arg(ap, unsigned); pf_putnum(&st, val, 16, width, pad, 0); break; } + case 's': { + const char* s = va_arg(ap, const char*); + if (!s) s = "(null)"; + int slen = 0; while (s[slen]) slen++; + for (int w = slen; w < width; w++) pf_putc(&st, ' '); + for (int j = 0; j < slen; j++) pf_putc(&st, s[j]); + break; + } + case 'c': { char c = (char)va_arg(ap, int); pf_putc(&st, c); break; } + case '%': pf_putc(&st, '%'); break; + default: pf_putc(&st, '%'); pf_putc(&st, *fmt); break; + } + if (*fmt) fmt++; + } + if (size > 0) { + if (st.pos < size) st.buf[st.pos] = '\0'; + else st.buf[size - 1] = '\0'; + } + return st.pos; +} + +static int snprintf(char* buf, int size, const char* fmt, ...) { + va_list ap; + va_start(ap, fmt); + int ret = vsnprintf(buf, size, fmt, ap); + va_end(ap); + return ret; +} + +// ---- IP/port parsing ---- + +static bool parse_uint16(const char* s, uint16_t* out) { + uint32_t val = 0; + if (*s == '\0') return false; + while (*s) { + if (*s < '0' || *s > '9') return false; + val = val * 10 + (*s - '0'); + if (val > 65535) return false; + s++; + } + *out = (uint16_t)val; + return true; +} + +// ---- Content type detection ---- + +static bool ends_with(const char* str, const char* suffix) { + int sl = slen(str); + int xl = slen(suffix); + if (xl > sl) return false; + for (int i = 0; i < xl; i++) { + char a = str[sl - xl + i]; + char b = suffix[i]; + // Case-insensitive for file extensions + if (a >= 'A' && a <= 'Z') a += 32; + if (b >= 'A' && b <= 'Z') b += 32; + if (a != b) return false; + } + return true; +} + +static const char* content_type_for(const char* path) { + if (ends_with(path, ".html") || ends_with(path, ".htm")) + return "text/html"; + if (ends_with(path, ".txt")) + return "text/plain"; + if (ends_with(path, ".css")) + return "text/css"; + if (ends_with(path, ".js")) + return "application/javascript"; + return "application/octet-stream"; +} + +// ---- HTTP response helpers ---- + +// Send a complete HTTP response with headers and body +static void send_response(int clientFd, int statusCode, const char* statusText, + const char* contentType, const char* body, int bodyLen) { + char header[512]; + int hlen = snprintf(header, sizeof(header), + "HTTP/1.0 %d %s\r\n" + "Content-Type: %s\r\n" + "Content-Length: %d\r\n" + "Connection: close\r\n" + "Server: ZenithOS/1.0\r\n" + "\r\n", + statusCode, statusText, contentType, bodyLen); + + zenith::send(clientFd, header, hlen); + if (bodyLen > 0) { + zenith::send(clientFd, body, bodyLen); + } +} + +// Send a file from the VFS +static int send_file_response(int clientFd, const char* vfsPath, const char* urlPath) { + int handle = zenith::open(vfsPath); + if (handle < 0) return -1; + + uint64_t size = zenith::getsize(handle); + const char* ctype = content_type_for(urlPath); + + // Send header + char header[512]; + int hlen = snprintf(header, sizeof(header), + "HTTP/1.0 200 OK\r\n" + "Content-Type: %s\r\n" + "Content-Length: %u\r\n" + "Connection: close\r\n" + "Server: ZenithOS/1.0\r\n" + "\r\n", + ctype, (unsigned)size); + zenith::send(clientFd, header, hlen); + + // Send file body in chunks + uint8_t buf[512]; + uint64_t offset = 0; + while (offset < size) { + uint64_t chunk = size - offset; + if (chunk > sizeof(buf)) chunk = sizeof(buf); + int bytesRead = zenith::read(handle, buf, offset, chunk); + if (bytesRead <= 0) break; + zenith::send(clientFd, buf, bytesRead); + offset += bytesRead; + } + + zenith::close(handle); + return (int)size; +} + +// ---- Request parsing ---- + +// Extract the request path from "GET /path HTTP/1.x\r\n..." +// Returns length of path written, or -1 on parse error +static int parse_request_path(const char* req, int reqLen, char* pathOut, int pathMax) { + // Find "GET " + if (reqLen < 4) return -1; + if (req[0] != 'G' || req[1] != 'E' || req[2] != 'T' || req[3] != ' ') + return -1; + + int i = 4; + int j = 0; + while (i < reqLen && req[i] != ' ' && req[i] != '\r' && req[i] != '\n') { + if (j < pathMax - 1) pathOut[j++] = req[i]; + i++; + } + pathOut[j] = '\0'; + return j; +} + +// ---- Logging ---- + +static void log_request(const char* method, const char* path, int status, int bodyLen) { + // Get timestamp + Zenith::DateTime dt; + zenith::gettime(&dt); + + char msg[256]; + snprintf(msg, sizeof(msg), "[%02d:%02d:%02d] %s %s -> %d (%d bytes)\n", + (int)dt.Hour, (int)dt.Minute, (int)dt.Second, + method, path, status, bodyLen); + zenith::print(msg); +} + +// ---- Page generators ---- + +static int generate_index_page(char* buf, int bufSize) { + Zenith::SysInfo info; + zenith::get_info(&info); + + uint64_t ms = zenith::get_milliseconds(); + uint64_t secs = ms / 1000; + uint64_t mins = secs / 60; + uint64_t hours = mins / 60; + secs %= 60; + mins %= 60; + + return snprintf(buf, bufSize, + "\n" + "\n" + "ZenithOS Web Server\n" + "\n" + "

ZenithOS Web Server

\n" + "

Welcome! This page is being served by httpd running on ZenithOS.

\n" + "

System Information

\n" + "\n" + "\n" + "\n" + "\n" + "
OS:%s
Version:%s
Uptime:%uh %um %us
\n" + "

Browse Files

\n" + "

Browse VFS files

\n" + "\n" + "\n", + info.osName, info.osVersion, + (unsigned)hours, (unsigned)mins, (unsigned)secs); +} + +static int generate_404_page(char* buf, int bufSize, const char* path) { + return snprintf(buf, bufSize, + "\n" + "\n" + "404 Not Found\n" + "\n" + "

404 Not Found

\n" + "

The requested path %s was not found on this server.

\n" + "

Back to home

\n" + "\n" + "\n", + path); +} + +static int generate_dir_listing(char* buf, int bufSize, const char* urlPath, const char* vfsDir) { + int pos = 0; + + pos += snprintf(buf + pos, bufSize - pos, + "\n" + "\n" + "Index of %s\n" + "\n" + "

Index of %s

\n" + "
\n" + "
    \n", + urlPath, urlPath); + + // Add parent directory link if not at /files/ + if (!streq(urlPath, "/files/")) { + pos += snprintf(buf + pos, bufSize - pos, + "
  • ..
  • \n"); + } + + // List directory entries + const char* entries[64]; + int count = zenith::readdir(vfsDir, entries, 64); + + // Find the prefix to strip from entry names + // vfsDir is like "0:/" or "0:/subdir" + // entries come back as "subdir/file" for "0:/subdir" + // or "file" for "0:/" + // The prefix in entry names is the part after "0:/" + const char* dirRel = vfsDir + 3; // skip "0:/" + int dirRelLen = slen(dirRel); + + for (int i = 0; i < count && pos < bufSize - 128; i++) { + // Extract just the filename portion + const char* name = entries[i]; + // If directory is not root, entries have "dir/name" format — strip the dir prefix + if (dirRelLen > 0 && starts_with(name, dirRel)) { + name = name + dirRelLen; + if (*name == '/') name++; + } + if (*name == '\0') continue; + + // Build the URL for this entry + pos += snprintf(buf + pos, bufSize - pos, + "
  • %s
  • \n", + urlPath, name, name); + } + + pos += snprintf(buf + pos, bufSize - pos, + "
\n" + "
\n" + "

ZenithOS httpd

\n" + "\n" + "\n"); + + return pos; +} + +// ---- Request handler ---- + +static void handle_client(int clientFd) { + // Read request (HTTP requests are small, 4 KB is plenty) + char reqBuf[4096]; + int reqLen = 0; + int idleCount = 0; + + // Read until we get the full header (ends with \r\n\r\n) + while (reqLen < (int)sizeof(reqBuf) - 1) { + int r = zenith::recv(clientFd, reqBuf + reqLen, sizeof(reqBuf) - 1 - reqLen); + if (r > 0) { + reqLen += r; + idleCount = 0; + // Check if we have the full header + bool done = false; + for (int i = 0; i + 3 < reqLen; i++) { + if (reqBuf[i] == '\r' && reqBuf[i+1] == '\n' && + reqBuf[i+2] == '\r' && reqBuf[i+3] == '\n') { + done = true; + break; + } + } + if (done) break; + } else if (r == 0) { + break; // Connection closed + } else { + idleCount++; + if (idleCount > 500) break; // Timeout + zenith::yield(); + } + } + reqBuf[reqLen] = '\0'; + + if (reqLen == 0) { + zenith::closesocket(clientFd); + return; + } + + // Parse request path + char path[256]; + if (parse_request_path(reqBuf, reqLen, path, sizeof(path)) < 0) { + // Bad request + static char body[] = "

400 Bad Request

"; + send_response(clientFd, 400, "Bad Request", "text/html", body, slen(body)); + log_request("???", "???", 400, slen(body)); + zenith::closesocket(clientFd); + return; + } + + // Route the request + static char pageBuf[16384]; + + if (streq(path, "/")) { + // Try to serve 0:/www/index.html from disk first + int handle = zenith::open("0:/www/index.html"); + if (handle >= 0) { + zenith::close(handle); + int bodyLen = send_file_response(clientFd, "0:/www/index.html", "/index.html"); + log_request("GET", path, 200, bodyLen); + } else { + // Fall back to built-in index page + int bodyLen = generate_index_page(pageBuf, sizeof(pageBuf)); + send_response(clientFd, 200, "OK", "text/html", pageBuf, bodyLen); + log_request("GET", path, 200, bodyLen); + } + + } else if (streq(path, "/files") || streq(path, "/files/")) { + // Root directory listing + int bodyLen = generate_dir_listing(pageBuf, sizeof(pageBuf), "/files/", "0:/"); + send_response(clientFd, 200, "OK", "text/html", pageBuf, bodyLen); + log_request("GET", path, 200, bodyLen); + + } else if (starts_with(path, "/files/")) { + // Serve file or directory from VFS + const char* relPath = path + 7; // skip "/files/" + + // Build VFS path: "0:/" + char vfsPath[256]; + int pi = 0; + vfsPath[pi++] = '0'; vfsPath[pi++] = ':'; vfsPath[pi++] = '/'; + int ri = 0; + while (relPath[ri] && pi < (int)sizeof(vfsPath) - 1) { + vfsPath[pi++] = relPath[ri++]; + } + // Strip trailing slash for VFS lookup + if (pi > 3 && vfsPath[pi-1] == '/') pi--; + vfsPath[pi] = '\0'; + + // Try to open as file first + int handle = zenith::open(vfsPath); + if (handle >= 0) { + // It's a file — serve it + zenith::close(handle); + int bodyLen = send_file_response(clientFd, vfsPath, path); + if (bodyLen >= 0) { + log_request("GET", path, 200, bodyLen); + } else { + static char body[] = "

500 Internal Server Error

"; + send_response(clientFd, 500, "Internal Server Error", "text/html", body, slen(body)); + log_request("GET", path, 500, slen(body)); + } + } else { + // Try as directory + const char* entries[64]; + int count = zenith::readdir(vfsPath, entries, 64); + if (count >= 0) { + // Make sure urlPath ends with / + char urlPath[256]; + int up = 0; + int pp = 0; + while (path[pp] && up < (int)sizeof(urlPath) - 2) urlPath[up++] = path[pp++]; + if (up > 0 && urlPath[up-1] != '/') urlPath[up++] = '/'; + urlPath[up] = '\0'; + + int bodyLen = generate_dir_listing(pageBuf, sizeof(pageBuf), urlPath, vfsPath); + send_response(clientFd, 200, "OK", "text/html", pageBuf, bodyLen); + log_request("GET", path, 200, bodyLen); + } else { + // Not found + int bodyLen = generate_404_page(pageBuf, sizeof(pageBuf), path); + send_response(clientFd, 404, "Not Found", "text/html", pageBuf, bodyLen); + log_request("GET", path, 404, bodyLen); + } + } + + } else { + // 404 for anything else + int bodyLen = generate_404_page(pageBuf, sizeof(pageBuf), path); + send_response(clientFd, 404, "Not Found", "text/html", pageBuf, bodyLen); + log_request("GET", path, 404, bodyLen); + } + + zenith::closesocket(clientFd); +} + +// ---- Entry point ---- + +extern "C" void _start() { + // Parse arguments: [port] + char argbuf[64]; + zenith::getargs(argbuf, sizeof(argbuf)); + const char* arg = skip_spaces(argbuf); + + uint16_t port = 80; + if (*arg) { + if (!parse_uint16(arg, &port)) { + zenith::print("Invalid port: "); + zenith::print(arg); + zenith::putchar('\n'); + zenith::exit(1); + } + } + + // Create server socket + int listenFd = zenith::socket(Zenith::SOCK_TCP); + if (listenFd < 0) { + zenith::print("Error: failed to create socket\n"); + zenith::exit(1); + } + + // Bind + if (zenith::bind(listenFd, port) < 0) { + zenith::print("Error: failed to bind to port "); + char tmp[8]; + snprintf(tmp, sizeof(tmp), "%d", (int)port); + zenith::print(tmp); + zenith::putchar('\n'); + zenith::closesocket(listenFd); + zenith::exit(1); + } + + // Listen + if (zenith::listen(listenFd) < 0) { + zenith::print("Error: failed to listen\n"); + zenith::closesocket(listenFd); + zenith::exit(1); + } + + char msg[128]; + snprintf(msg, sizeof(msg), "ZenithOS httpd listening on port %d\n", (int)port); + zenith::print(msg); + zenith::print("Press Ctrl+Q between requests to stop.\n\n"); + + bool running = true; + while (running) { + // Check for Ctrl+Q before blocking on accept + while (zenith::is_key_available()) { + Zenith::KeyEvent ev; + zenith::getkey(&ev); + if (ev.pressed && ev.ctrl && ev.ascii == 'q') { + running = false; + break; + } + } + if (!running) break; + + // Accept next client (blocks until a connection arrives) + int clientFd = zenith::accept(listenFd); + if (clientFd < 0) { + zenith::print("Warning: accept failed\n"); + zenith::yield(); + continue; + } + + handle_client(clientFd); + + // After serving, check for Ctrl+Q + while (zenith::is_key_available()) { + Zenith::KeyEvent ev; + zenith::getkey(&ev); + if (ev.pressed && ev.ctrl && ev.ascii == 'q') { + running = false; + break; + } + } + } + + zenith::print("\nShutting down httpd...\n"); + zenith::closesocket(listenFd); + zenith::exit(0); +} diff --git a/programs/src/ifconfig/main.cpp b/programs/src/ifconfig/main.cpp new file mode 100644 index 0000000..e08571d --- /dev/null +++ b/programs/src/ifconfig/main.cpp @@ -0,0 +1,149 @@ +/* + * main.cpp + * ifconfig - Show or set network configuration + * Copyright (c) 2025-2026 Daniel Hammer +*/ + +#include +#include + +using zenith::starts_with; +using zenith::skip_spaces; + +static void print_int(uint64_t n) { + if (n == 0) { + zenith::putchar('0'); + return; + } + char buf[20]; + int i = 0; + while (n > 0) { + buf[i++] = '0' + (n % 10); + n /= 10; + } + for (int j = i - 1; j >= 0; j--) { + zenith::putchar(buf[j]); + } +} + +static void print_ip(uint32_t ip) { + print_int(ip & 0xFF); + zenith::putchar('.'); + print_int((ip >> 8) & 0xFF); + zenith::putchar('.'); + print_int((ip >> 16) & 0xFF); + zenith::putchar('.'); + print_int((ip >> 24) & 0xFF); +} + +static bool parse_ip(const char* s, uint32_t* out) { + uint32_t octets[4]; + int idx = 0; + uint32_t val = 0; + bool hasDigit = false; + + for (int i = 0; ; i++) { + char c = s[i]; + if (c >= '0' && c <= '9') { + val = val * 10 + (c - '0'); + if (val > 255) return false; + hasDigit = true; + } else if (c == '.' || c == '\0') { + if (!hasDigit || idx >= 4) return false; + octets[idx++] = val; + val = 0; + hasDigit = false; + if (c == '\0') break; + } else { + return false; + } + } + + if (idx != 4) return false; + *out = octets[0] | (octets[1] << 8) | (octets[2] << 16) | (octets[3] << 24); + return true; +} + +extern "C" void _start() { + char args[256]; + int len = zenith::getargs(args, sizeof(args)); + + if (len <= 0 || args[0] == '\0') { + // Show current network configuration + Zenith::NetCfg cfg; + zenith::get_netcfg(&cfg); + zenith::print(" IP Address: "); + print_ip(cfg.ipAddress); + zenith::putchar('\n'); + zenith::print(" Subnet Mask: "); + print_ip(cfg.subnetMask); + zenith::putchar('\n'); + zenith::print(" Gateway: "); + print_ip(cfg.gateway); + zenith::putchar('\n'); + zenith::exit(0); + } + + if (!starts_with(args, "set ")) { + zenith::print("Usage: ifconfig Show network config\n"); + zenith::print(" ifconfig set \n"); + zenith::exit(1); + } + + // Parse: set + const char* p = skip_spaces(args + 4); + + // Parse IP + char tok[32]; + int i = 0; + while (p[i] && p[i] != ' ' && i < 31) { tok[i] = p[i]; i++; } + tok[i] = '\0'; + uint32_t ip; + if (!parse_ip(tok, &ip)) { + zenith::print("Invalid IP address: "); + zenith::print(tok); + zenith::putchar('\n'); + zenith::exit(1); + } + p = skip_spaces(p + i); + + // Parse subnet mask + i = 0; + while (p[i] && p[i] != ' ' && i < 31) { tok[i] = p[i]; i++; } + tok[i] = '\0'; + uint32_t mask; + if (!parse_ip(tok, &mask)) { + zenith::print("Invalid subnet mask: "); + zenith::print(tok); + zenith::putchar('\n'); + zenith::exit(1); + } + p = skip_spaces(p + i); + + // Parse gateway + i = 0; + while (p[i] && p[i] != ' ' && i < 31) { tok[i] = p[i]; i++; } + tok[i] = '\0'; + uint32_t gw; + if (!parse_ip(tok, &gw)) { + zenith::print("Invalid gateway: "); + zenith::print(tok); + zenith::putchar('\n'); + zenith::exit(1); + } + + Zenith::NetCfg cfg; + cfg.ipAddress = ip; + cfg.subnetMask = mask; + cfg.gateway = gw; + if (zenith::set_netcfg(&cfg) < 0) { + zenith::print("Error: failed to set network config\n"); + zenith::exit(1); + } + + zenith::print("Network config updated:\n"); + zenith::print(" IP Address: "); print_ip(ip); zenith::putchar('\n'); + zenith::print(" Subnet Mask: "); print_ip(mask); zenith::putchar('\n'); + zenith::print(" Gateway: "); print_ip(gw); zenith::putchar('\n'); + zenith::exit(0); +} diff --git a/programs/src/info/main.cpp b/programs/src/info/main.cpp new file mode 100644 index 0000000..59abe65 --- /dev/null +++ b/programs/src/info/main.cpp @@ -0,0 +1,36 @@ +/* + * main.cpp + * info - Show system information + * Copyright (c) 2025-2026 Daniel Hammer +*/ + +#include + +static void print_int(uint64_t n) { + if (n == 0) { + zenith::putchar('0'); + return; + } + char buf[20]; + int i = 0; + while (n > 0) { + buf[i++] = '0' + (n % 10); + n /= 10; + } + for (int j = i - 1; j >= 0; j--) { + zenith::putchar(buf[j]); + } +} + +extern "C" void _start() { + Zenith::SysInfo info; + zenith::get_info(&info); + zenith::print(info.osName); + zenith::print(" v"); + zenith::print(info.osVersion); + zenith::putchar('\n'); + zenith::print("Syscall API version: "); + print_int(info.apiVersion); + zenith::putchar('\n'); + zenith::exit(0); +} diff --git a/programs/src/init/main.cpp b/programs/src/init/main.cpp new file mode 100644 index 0000000..364f4e8 --- /dev/null +++ b/programs/src/init/main.cpp @@ -0,0 +1,164 @@ +/* + * main.cpp + * Init system for ZenithOS (PID 0) + * Chains system services then launches the shell. + * Copyright (c) 2026 Daniel Hammer +*/ + +#include + +// ---- Minimal snprintf ---- + +using va_list = __builtin_va_list; +#define va_start __builtin_va_start +#define va_end __builtin_va_end +#define va_arg __builtin_va_arg + +struct PfState { char* buf; int pos; int max; }; + +static void pf_putc(PfState* st, char c) { + if (st->pos < st->max) st->buf[st->pos] = c; + st->pos++; +} + +static void pf_putnum(PfState* st, unsigned long val, int base, int width, char pad, int neg) { + char tmp[24]; int i = 0; + const char* digits = "0123456789abcdef"; + if (val == 0) { tmp[i++] = '0'; } + else { while (val > 0) { tmp[i++] = digits[val % base]; val /= base; } } + int total = (neg ? 1 : 0) + i; + if (neg && pad == '0') pf_putc(st, '-'); + for (int w = total; w < width; w++) pf_putc(st, pad); + if (neg && pad != '0') pf_putc(st, '-'); + while (i > 0) pf_putc(st, tmp[--i]); +} + +static int vsnprintf(char* buf, int size, const char* fmt, va_list ap) { + PfState st; st.buf = buf; st.pos = 0; st.max = size > 0 ? size - 1 : 0; + while (*fmt) { + if (*fmt != '%') { pf_putc(&st, *fmt++); continue; } + fmt++; + char pad = ' '; + if (*fmt == '0') { pad = '0'; fmt++; } + int width = 0; + while (*fmt >= '0' && *fmt <= '9') { width = width * 10 + (*fmt - '0'); fmt++; } + if (*fmt == 'l') fmt++; + switch (*fmt) { + case 'd': case 'i': { + long val = va_arg(ap, int); + int neg = 0; unsigned long uval; + if (val < 0) { neg = 1; uval = (unsigned long)(-val); } else uval = (unsigned long)val; + pf_putnum(&st, uval, 10, width, pad, neg); break; + } + case 'u': { unsigned val = va_arg(ap, unsigned); pf_putnum(&st, val, 10, width, pad, 0); break; } + case 'x': { unsigned val = va_arg(ap, unsigned); pf_putnum(&st, val, 16, width, pad, 0); break; } + case 's': { + const char* s = va_arg(ap, const char*); if (!s) s = "(null)"; + int slen = 0; while (s[slen]) slen++; + for (int w = slen; w < width; w++) pf_putc(&st, ' '); + for (int j = 0; j < slen; j++) pf_putc(&st, s[j]); + break; + } + case 'c': { char c = (char)va_arg(ap, int); pf_putc(&st, c); break; } + case '%': pf_putc(&st, '%'); break; + default: pf_putc(&st, '%'); pf_putc(&st, *fmt); break; + } + if (*fmt) fmt++; + } + if (size > 0) { if (st.pos < size) st.buf[st.pos] = '\0'; else st.buf[size - 1] = '\0'; } + return st.pos; +} + +static int snprintf(char* buf, int size, const char* fmt, ...) { + va_list ap; va_start(ap, fmt); + int ret = vsnprintf(buf, size, fmt, ap); + va_end(ap); return ret; +} + +// ---- ANSI color codes ---- + +#define C_RESET "\033[0m" +#define C_BOLD "\033[1m" +#define C_DIM "\033[2m" +#define C_RED "\033[31m" +#define C_GREEN "\033[32m" +#define C_YELLOW "\033[33m" +#define C_BLUE "\033[34m" +#define C_CYAN "\033[36m" +#define C_WHITE "\033[37m" + +// ---- Logging ---- + +static void log_timestamp(char* buf, int size) { + Zenith::DateTime dt; + zenith::gettime(&dt); + snprintf(buf, size, "%02d:%02d:%02d", dt.Hour, dt.Minute, dt.Second); +} + +enum LogLevel { LOG_OK, LOG_INFO, LOG_WARN, LOG_ERR }; + +static void log(LogLevel level, const char* msg) { + char line[512]; + char ts[16]; + log_timestamp(ts, sizeof(ts)); + + const char* tag; + const char* color; + switch (level) { + case LOG_OK: tag = " OK "; color = C_GREEN; break; + case LOG_INFO: tag = " INFO "; color = C_CYAN; break; + case LOG_WARN: tag = " WARN "; color = C_YELLOW; break; + case LOG_ERR: tag = " FAIL "; color = C_RED; break; + } + + snprintf(line, sizeof(line), + C_DIM "%s" C_RESET " %s%s" C_RESET " " C_BOLD "init" C_RESET " %s\n", + ts, color, tag, msg); + zenith::print(line); +} + +static void log_ok(const char* msg) { log(LOG_OK, msg); } +static void log_info(const char* msg) { log(LOG_INFO, msg); } +static void log_warn(const char* msg) { log(LOG_WARN, msg); } +static void log_err(const char* msg) { log(LOG_ERR, msg); } + +// ---- Service runner ---- + +static bool run_service(const char* path, const char* name) { + char msg[128]; + + snprintf(msg, sizeof(msg), "Starting %s", name); + log_info(msg); + + int pid = zenith::spawn(path); + if (pid < 0) { + snprintf(msg, sizeof(msg), "Failed to start %s", name); + log_err(msg); + return false; + } + + zenith::waitpid(pid); + + snprintf(msg, sizeof(msg), "%s finished (pid %d)", name, pid); + log_ok(msg); + return true; +} + +// ---- Main ---- + +extern "C" void _start() { + + log_info("Init system starting (PID 0)"); + + // ---- Stage 1: Network configuration ---- + run_service("0:/os/dhcp.elf", "dhcp"); + + // ---- Stage 2: Interactive shell ---- + run_service("0:/os/shell.elf", "shell"); + + log_warn("All services exited"); + + for (;;) { + zenith::yield(); + } +} diff --git a/programs/src/irc/main.cpp b/programs/src/irc/main.cpp index 1221737..a3ab2a9 100644 --- a/programs/src/irc/main.cpp +++ b/programs/src/irc/main.cpp @@ -6,6 +6,16 @@ */ #include +#include + +using zenith::slen; +using zenith::streq; +using zenith::starts_with; +using zenith::skip_spaces; +using zenith::strcpy; +using zenith::strncpy; +using zenith::memcpy; +using zenith::memmove; // ---- Minimal snprintf (no libc available) ---- @@ -91,51 +101,6 @@ static int snprintf(char* buf, int size, const char* fmt, ...) { return ret; } -// ---- String utilities ---- - -static int slen(const char* s) { - int n = 0; - while (s[n]) n++; - return n; -} - -static bool streq(const char* a, const char* b) { - while (*a && *b) { if (*a != *b) return false; a++; b++; } - return *a == *b; -} - -static bool starts_with(const char* str, const char* prefix) { - while (*prefix) { if (*str != *prefix) return false; str++; prefix++; } - return true; -} - -static const char* skip_spaces(const char* s) { - while (*s == ' ') s++; - return s; -} - -static void strcpy(char* dst, const char* src) { - while (*src) *dst++ = *src++; - *dst = '\0'; -} - -static void strncpy(char* dst, const char* src, int max) { - int i = 0; - while (src[i] && i < max - 1) { dst[i] = src[i]; i++; } - dst[i] = '\0'; -} - -static void memcpy(void* dst, const void* src, int n) { - auto d = (char*)dst; auto s = (const char*)src; - for (int i = 0; i < n; i++) d[i] = s[i]; -} - -static void memmove(void* dst, const void* src, int n) { - auto d = (char*)dst; auto s = (const char*)src; - if (d < s) { for (int i = 0; i < n; i++) d[i] = s[i]; } - else { for (int i = n - 1; i >= 0; i--) d[i] = s[i]; } -} - // Case-insensitive comparison for IRC commands static bool streqi(const char* a, const char* b) { while (*a && *b) { diff --git a/programs/src/man/main.cpp b/programs/src/man/main.cpp index 70e8316..8660e04 100644 --- a/programs/src/man/main.cpp +++ b/programs/src/man/main.cpp @@ -7,27 +7,11 @@ #include #include +#include -// ---- Utility functions ---- - -static bool starts_with(const char* str, const char* prefix) { - while (*prefix) { - if (*str != *prefix) return false; - str++; prefix++; - } - return true; -} - -static const char* skip_spaces(const char* s) { - while (*s == ' ') s++; - return s; -} - -static int slen(const char* s) { - int n = 0; - while (s[n]) n++; - return n; -} +using zenith::slen; +using zenith::starts_with; +using zenith::skip_spaces; static void print_int(uint64_t n) { if (n == 0) { diff --git a/programs/src/ping/main.cpp b/programs/src/ping/main.cpp new file mode 100644 index 0000000..2f0990e --- /dev/null +++ b/programs/src/ping/main.cpp @@ -0,0 +1,101 @@ +/* + * main.cpp + * ping - Send ICMP echo requests + * Copyright (c) 2025-2026 Daniel Hammer +*/ + +#include + +static void print_int(uint64_t n) { + if (n == 0) { + zenith::putchar('0'); + return; + } + char buf[20]; + int i = 0; + while (n > 0) { + buf[i++] = '0' + (n % 10); + n /= 10; + } + for (int j = i - 1; j >= 0; j--) { + zenith::putchar(buf[j]); + } +} + +static bool parse_ip(const char* s, uint32_t* out) { + uint32_t octets[4]; + int idx = 0; + uint32_t val = 0; + bool hasDigit = false; + + for (int i = 0; ; i++) { + char c = s[i]; + if (c >= '0' && c <= '9') { + val = val * 10 + (c - '0'); + if (val > 255) return false; + hasDigit = true; + } else if (c == '.' || c == '\0') { + if (!hasDigit || idx >= 4) return false; + octets[idx++] = val; + val = 0; + hasDigit = false; + if (c == '\0') break; + } else { + return false; + } + } + + if (idx != 4) return false; + *out = octets[0] | (octets[1] << 8) | (octets[2] << 16) | (octets[3] << 24); + return true; +} + +static void print_ip(uint32_t ip) { + print_int(ip & 0xFF); + zenith::putchar('.'); + print_int((ip >> 8) & 0xFF); + zenith::putchar('.'); + print_int((ip >> 16) & 0xFF); + zenith::putchar('.'); + print_int((ip >> 24) & 0xFF); +} + +extern "C" void _start() { + char args[256]; + int len = zenith::getargs(args, sizeof(args)); + + if (len <= 0 || args[0] == '\0') { + zenith::print("Usage: ping \n"); + zenith::exit(1); + } + + uint32_t ip; + if (!parse_ip(args, &ip)) { + zenith::print("Invalid IP address: "); + zenith::print(args); + zenith::putchar('\n'); + zenith::exit(1); + } + + zenith::print("PING "); + print_ip(ip); + zenith::putchar('\n'); + + for (int i = 0; i < 4; i++) { + int32_t rtt = zenith::ping(ip, 3000); + if (rtt < 0) { + zenith::print(" Request timed out\n"); + } else { + zenith::print(" Reply from "); + print_ip(ip); + zenith::print(": time="); + print_int((uint64_t)rtt); + zenith::print("ms\n"); + } + if (i < 3) { + zenith::sleep_ms(1000); + } + } + + zenith::exit(0); +} diff --git a/programs/src/reset/main.cpp b/programs/src/reset/main.cpp new file mode 100644 index 0000000..c60760a --- /dev/null +++ b/programs/src/reset/main.cpp @@ -0,0 +1,12 @@ +/* + * main.cpp + * reset - Reboot the system + * Copyright (c) 2025-2026 Daniel Hammer +*/ + +#include + +extern "C" void _start() { + zenith::print("Rebooting...\n"); + zenith::reset(); +} diff --git a/programs/src/shell/main.cpp b/programs/src/shell/main.cpp index 2181688..f67f525 100644 --- a/programs/src/shell/main.cpp +++ b/programs/src/shell/main.cpp @@ -5,52 +5,33 @@ */ #include +#include -static bool streq(const char* a, const char* b) { - while (*a && *b) { - if (*a != *b) return false; - a++; b++; +using zenith::slen; +using zenith::streq; +using zenith::starts_with; +using zenith::skip_spaces; + +static void scopy(char* dst, const char* src, int maxLen) { + int i = 0; + while (src[i] && i < maxLen - 1) { dst[i] = src[i]; i++; } + dst[i] = '\0'; +} + +static void scat(char* dst, const char* src, int maxLen) { + int dLen = slen(dst); + int i = 0; + while (src[i] && dLen + i < maxLen - 1) { + dst[dLen + i] = src[i]; + i++; } - return *a == *b; -} - -static bool starts_with(const char* str, const char* prefix) { - while (*prefix) { - if (*str != *prefix) return false; - str++; prefix++; - } - return true; -} - -static const char* skip_spaces(const char* s) { - while (*s == ' ') s++; - return s; -} - -static int slen(const char* s) { - int n = 0; - while (s[n]) n++; - return n; + dst[dLen + i] = '\0'; } // Current working directory (relative to 0:/). // "" = root, "man" = 0:/man, "man/sub" = 0:/man/sub static char cwd[128] = ""; -// Build full VFS path: "0:/" + cwd + "/" + name -static void resolve_path(const char* name, char* out, int outMax) { - int i = 0; - out[i++] = '0'; out[i++] = ':'; out[i++] = '/'; - if (cwd[0]) { - int j = 0; - while (cwd[j] && i < outMax - 2) out[i++] = cwd[j++]; - out[i++] = '/'; - } - int j = 0; - while (name[j] && i < outMax - 1) out[i++] = name[j++]; - out[i] = '\0'; -} - // Build VFS directory path: "0:/" or "0:/" static void build_dir_path(const char* dir, char* out, int outMax) { int i = 0; @@ -62,58 +43,100 @@ static void build_dir_path(const char* dir, char* out, int outMax) { out[i] = '\0'; } -static void print_int(uint64_t n) { - if (n == 0) { - zenith::putchar('0'); - return; - } - char buf[20]; - int i = 0; - while (n > 0) { - buf[i++] = '0' + (n % 10); - n /= 10; - } - for (int j = i - 1; j >= 0; j--) { - zenith::putchar(buf[j]); +// ---- Command history ---- + +static constexpr int HISTORY_MAX = 32; +static char history[HISTORY_MAX][256]; +static int history_count = 0; +static int history_next = 0; // ring-buffer write index + +static void history_add(const char* line) { + if (line[0] == '\0') return; + // Don't add duplicate of last entry + if (history_count > 0) { + int prev = (history_next + HISTORY_MAX - 1) % HISTORY_MAX; + if (streq(history[prev], line)) return; } + scopy(history[history_next], line, 256); + history_next = (history_next + 1) % HISTORY_MAX; + if (history_count < HISTORY_MAX) history_count++; } +// Get history entry by index (0 = most recent, 1 = one before that, ...) +static const char* history_get(int idx) { + if (idx < 0 || idx >= history_count) return nullptr; + int pos = (history_next + HISTORY_MAX - 1 - idx) % HISTORY_MAX; + return history[pos]; +} + +// ---- Prompt ---- + static void prompt() { zenith::print("0:/"); if (cwd[0]) zenith::print(cwd); zenith::print("> "); } +// ---- Erase current input line on screen ---- + +static void erase_input(int len) { + // Move cursor to start of input and overwrite with spaces + for (int i = 0; i < len; i++) zenith::putchar('\b'); + for (int i = 0; i < len; i++) zenith::putchar(' '); + for (int i = 0; i < len; i++) zenith::putchar('\b'); +} + +// ---- Replace visible line with new content ---- + +static void replace_line(char* line, int* pos, const char* newContent) { + int oldLen = *pos; + erase_input(oldLen); + int newLen = slen(newContent); + if (newLen > 255) newLen = 255; + for (int i = 0; i < newLen; i++) { + line[i] = newContent[i]; + zenith::putchar(newContent[i]); + } + line[newLen] = '\0'; + *pos = newLen; +} + +// ---- Builtin: help ---- + static void cmd_help() { - zenith::print("Available commands:\n"); + zenith::print("Shell builtins:\n"); zenith::print(" help Show this help message\n"); - zenith::print(" info Show system information\n"); - zenith::print(" man View manual pages\n"); zenith::print(" ls [dir] List files in directory\n"); zenith::print(" cd [dir] Change working directory\n"); + zenith::print(" exit Exit the shell\n"); + zenith::print("\n"); + zenith::print("System commands:\n"); + zenith::print(" man View manual pages\n"); zenith::print(" cat Display file contents\n"); - zenith::print(" run Spawn a new process from an ELF file\n"); - zenith::print(" ping Send ICMP echo requests\n"); - zenith::print(" tcpconnect Connect to a TCP server\n"); + zenith::print(" edit [file] Text editor\n"); + zenith::print(" info Show system information\n"); zenith::print(" date Show current date and time\n"); - zenith::print(" uptime Show uptime in milliseconds\n"); + zenith::print(" uptime Show uptime\n"); zenith::print(" clear Clear the screen\n"); zenith::print(" reset Reboot the system\n"); zenith::print(" shutdown Shut down the system\n"); - zenith::print(" exit Exit the shell\n"); + zenith::print("\n"); + zenith::print("Network commands:\n"); + zenith::print(" ping Send ICMP echo requests\n"); + zenith::print(" ifconfig Show/set network configuration\n"); + zenith::print(" tcpconnect Connect to a TCP server\n"); + zenith::print(" irc IRC client\n"); + zenith::print(" dhcp DHCP client\n"); + zenith::print(" fetch HTTP client\n"); + zenith::print(" httpd HTTP server\n"); + zenith::print("\n"); + zenith::print("Games:\n"); + zenith::print(" doom DOOM\n"); + zenith::print("\n"); + zenith::print("Any .elf on the ramdisk is executable.\n"); } -static void cmd_info() { - Zenith::SysInfo info; - zenith::get_info(&info); - zenith::print(info.osName); - zenith::print(" v"); - zenith::print(info.osVersion); - zenith::print("\n"); - zenith::print("Syscall API version: "); - print_int(info.apiVersion); - zenith::putchar('\n'); -} +// ---- Builtin: ls ---- static void cmd_ls(const char* arg) { arg = skip_spaces(arg); @@ -121,7 +144,7 @@ static void cmd_ls(const char* arg) { // Build the target directory (relative path from root) char dir[128]; if (*arg) { - // ls — combine cwd and arg + // ls -- combine cwd and arg if (cwd[0]) { int i = 0, j = 0; while (cwd[j] && i < 126) dir[i++] = cwd[j++]; @@ -135,7 +158,7 @@ static void cmd_ls(const char* arg) { dir[i] = '\0'; } } else { - // ls with no arg — use cwd + // ls with no arg -- use cwd int i = 0; while (cwd[i] && i < 126) { dir[i] = cwd[i]; i++; } dir[i] = '\0'; @@ -166,310 +189,26 @@ static void cmd_ls(const char* arg) { } } -static void cmd_cat(const char* arg) { - arg = skip_spaces(arg); - if (*arg == '\0') { - zenith::print("Usage: cat \n"); - return; - } - - char path[128]; - resolve_path(arg, path, sizeof(path)); - - int handle = zenith::open(path); - if (handle < 0) { - zenith::print("Error: cannot open '"); - zenith::print(arg); - zenith::print("'\n"); - return; - } - - uint64_t size = zenith::getsize(handle); - if (size == 0) { - zenith::close(handle); - return; - } - - // Read in chunks - uint8_t buf[512]; - uint64_t offset = 0; - while (offset < size) { - uint64_t chunk = size - offset; - if (chunk > sizeof(buf) - 1) chunk = sizeof(buf) - 1; - int bytesRead = zenith::read(handle, buf, offset, chunk); - if (bytesRead <= 0) break; - buf[bytesRead] = '\0'; - zenith::print((const char*)buf); - offset += bytesRead; - } - - zenith::close(handle); - zenith::putchar('\n'); -} - -static void cmd_uptime() { - uint64_t ms = zenith::get_milliseconds(); - uint64_t secs = ms / 1000; - uint64_t mins = secs / 60; - secs %= 60; - ms %= 1000; - - zenith::print("Uptime: "); - print_int(mins); - zenith::print("m "); - print_int(secs); - zenith::print("s "); - print_int(ms); - zenith::print("ms\n"); -} - -static bool parse_ip(const char* s, uint32_t* out) { - // Parse "a.b.c.d" into a uint32_t in network byte order (little-endian stored) - uint32_t octets[4]; - int idx = 0; - uint32_t val = 0; - bool hasDigit = false; - - for (int i = 0; ; i++) { - char c = s[i]; - if (c >= '0' && c <= '9') { - val = val * 10 + (c - '0'); - if (val > 255) return false; - hasDigit = true; - } else if (c == '.' || c == '\0') { - if (!hasDigit || idx >= 4) return false; - octets[idx++] = val; - val = 0; - hasDigit = false; - if (c == '\0') break; - } else { - return false; - } - } - - if (idx != 4) return false; - *out = octets[0] | (octets[1] << 8) | (octets[2] << 16) | (octets[3] << 24); - return true; -} - -static void print_ip(uint32_t ip) { - print_int(ip & 0xFF); - zenith::putchar('.'); - print_int((ip >> 8) & 0xFF); - zenith::putchar('.'); - print_int((ip >> 16) & 0xFF); - zenith::putchar('.'); - print_int((ip >> 24) & 0xFF); -} - -static void cmd_ping(const char* arg) { - arg = skip_spaces(arg); - if (*arg == '\0') { - zenith::print("Usage: ping \n"); - return; - } - - uint32_t ip; - if (!parse_ip(arg, &ip)) { - zenith::print("Invalid IP address: "); - zenith::print(arg); - zenith::putchar('\n'); - return; - } - - zenith::print("PING "); - print_ip(ip); - zenith::putchar('\n'); - - for (int i = 0; i < 4; i++) { - int32_t rtt = zenith::ping(ip, 3000); - if (rtt < 0) { - zenith::print(" Request timed out\n"); - } else { - zenith::print(" Reply from "); - print_ip(ip); - zenith::print(": time="); - print_int((uint64_t)rtt); - zenith::print("ms\n"); - } - if (i < 3) { - zenith::sleep_ms(1000); - } - } -} - -static bool parse_uint16(const char* s, uint16_t* out) { - uint32_t val = 0; - if (*s == '\0') return false; - while (*s) { - if (*s < '0' || *s > '9') return false; - val = val * 10 + (*s - '0'); - if (val > 65535) return false; - s++; - } - *out = (uint16_t)val; - return true; -} - -static void cmd_tcpconnect(const char* arg) { - arg = skip_spaces(arg); - if (*arg == '\0') { - zenith::print("Usage: tcpconnect \n"); - return; - } - - // Parse IP address (up to first space) - char ipStr[32]; - int i = 0; - while (arg[i] && arg[i] != ' ' && i < 31) { - ipStr[i] = arg[i]; - i++; - } - ipStr[i] = '\0'; - - uint32_t ip; - if (!parse_ip(ipStr, &ip)) { - zenith::print("Invalid IP address: "); - zenith::print(ipStr); - zenith::putchar('\n'); - return; - } - - // Parse port - const char* portStr = skip_spaces(arg + i); - if (*portStr == '\0') { - zenith::print("Usage: tcpconnect \n"); - return; - } - uint16_t port; - if (!parse_uint16(portStr, &port)) { - zenith::print("Invalid port: "); - zenith::print(portStr); - zenith::putchar('\n'); - return; - } - - // Create socket - int fd = zenith::socket(Zenith::SOCK_TCP); - if (fd < 0) { - zenith::print("Error: failed to create socket\n"); - return; - } - - zenith::print("Connecting to "); - print_ip(ip); - zenith::putchar(':'); - print_int(port); - zenith::print("...\n"); - - if (zenith::connect(fd, ip, port) < 0) { - zenith::print("Error: connection failed\n"); - zenith::closesocket(fd); - return; - } - - zenith::print("Connected! Type to send, Ctrl+Q to disconnect.\n"); - - // Interactive send/receive loop - char sendBuf[256]; - int sendPos = 0; - uint8_t recvBuf[512]; - - while (true) { - // Poll for received data (non-blocking) - int r = zenith::recv(fd, recvBuf, sizeof(recvBuf) - 1); - if (r < 0) { - zenith::print("\nConnection closed by remote.\n"); - break; - } - if (r > 0) { - recvBuf[r] = '\0'; - zenith::print((const char*)recvBuf); - } - - // Poll keyboard - if (zenith::is_key_available()) { - Zenith::KeyEvent ev; - zenith::getkey(&ev); - - if (!ev.pressed) continue; - - // Ctrl+Q to quit - if (ev.ctrl && (ev.ascii == 'q' || ev.ascii == 'Q')) { - zenith::print("\nDisconnecting...\n"); - break; - } - - if (ev.ascii == '\n') { - sendBuf[sendPos++] = '\n'; - zenith::putchar('\n'); - zenith::send(fd, sendBuf, sendPos); - sendPos = 0; - } else if (ev.ascii == '\b') { - if (sendPos > 0) { - sendPos--; - zenith::putchar('\b'); - zenith::putchar(' '); - zenith::putchar('\b'); - } - } else if (ev.ascii >= ' ' && sendPos < 254) { - sendBuf[sendPos++] = ev.ascii; - zenith::putchar(ev.ascii); - } - } else { - // No key and no data — yield to avoid busy-spinning - zenith::yield(); - } - } - - zenith::closesocket(fd); -} - -static void cmd_run(const char* arg) { - arg = skip_spaces(arg); - if (*arg == '\0') { - zenith::print("Usage: run [args...]\n"); - return; - } - - // Split filename from arguments at first space - char filename[128]; - int i = 0; - while (arg[i] && arg[i] != ' ' && i < 127) { - filename[i] = arg[i]; - i++; - } - filename[i] = '\0'; - - const char* args = nullptr; - if (arg[i] == ' ') { - args = skip_spaces(arg + i); - if (*args == '\0') args = nullptr; - } - - char path[128]; - resolve_path(filename, path, sizeof(path)); - - int pid = zenith::spawn(path, args); - if (pid < 0) { - zenith::print("Error: failed to spawn '"); - zenith::print(filename); - zenith::print("'\n"); - } else { - zenith::waitpid(pid); - } -} +// ---- Builtin: cd ---- static void cmd_cd(const char* arg) { arg = skip_spaces(arg); - // cd or cd / → go to root + // Strip trailing slashes from argument (ls shows dirs as "www/", user may type that) + static char argBuf[128]; + int aLen = 0; + while (arg[aLen] && aLen < 127) { argBuf[aLen] = arg[aLen]; aLen++; } + argBuf[aLen] = '\0'; + while (aLen > 0 && argBuf[aLen - 1] == '/') argBuf[--aLen] = '\0'; + arg = argBuf; + + // cd or cd / -> go to root if (*arg == '\0' || streq(arg, "/")) { cwd[0] = '\0'; return; } - // cd .. → go up one level + // cd .. -> go up one level if (streq(arg, "..")) { int len = slen(cwd); int last = -1; @@ -517,6 +256,8 @@ static void cmd_cd(const char* arg) { cwd[i] = '\0'; } +// ---- Builtin: man ---- + static void cmd_man(const char* arg) { arg = skip_spaces(arg); if (*arg == '\0') { @@ -526,7 +267,7 @@ static void cmd_man(const char* arg) { return; } - int pid = zenith::spawn("0:/man.elf", arg); + int pid = zenith::spawn("0:/os/man.elf", arg); if (pid < 0) { zenith::print("Error: failed to start man viewer\n"); } else { @@ -534,102 +275,155 @@ static void cmd_man(const char* arg) { } } -static void print_int_padded(uint64_t n) { - if (n < 10) zenith::putchar('0'); - print_int(n); +// ---- External command execution ---- + +// Try to spawn an ELF at the given path. Returns true on success. +static bool try_exec(const char* path, const char* args) { + // Check if the file exists before asking the kernel to load it + int h = zenith::open(path); + if (h < 0) return false; + zenith::close(h); + + int pid = zenith::spawn(path, args); + if (pid < 0) return false; + zenith::waitpid(pid); + return true; } -static const char* month_name(int m) { - static const char* months[] = { - "", "January", "February", "March", "April", "May", "June", - "July", "August", "September", "October", "November", "December" - }; - if (m >= 1 && m <= 12) return months[m]; - return "?"; +// Check if a string already has a VFS drive prefix (e.g. "0:/") +static bool has_drive_prefix(const char* s) { + return s[0] >= '0' && s[0] <= '9' && s[1] == ':'; } -static void cmd_date() { - Zenith::DateTime dt; - zenith::gettime(&dt); +// Resolve arguments: expand relative file paths against CWD. +// Tokens that already have a drive prefix (e.g. "0:/foo") are left as-is. +// Everything before the first space-delimited token that looks like a path +// option (starts with '-') is also left as-is. +static void resolve_args(const char* args, char* out, int outMax) { + if (!args || !args[0]) { out[0] = '\0'; return; } - print_int(dt.Day); - zenith::putchar(' '); - zenith::print(month_name(dt.Month)); - zenith::putchar(' '); - print_int(dt.Year); - zenith::print(", "); - print_int(dt.Hour); - zenith::putchar(':'); - print_int_padded(dt.Minute); - zenith::putchar(':'); - print_int_padded(dt.Second); - zenith::print(" UTC\n"); + int o = 0; + const char* p = args; + + while (*p && o < outMax - 1) { + // Skip spaces, copy them through + while (*p == ' ' && o < outMax - 1) { out[o++] = *p++; } + if (!*p) break; + + // Extract the token + const char* tokStart = p; + int tokLen = 0; + while (p[tokLen] && p[tokLen] != ' ') tokLen++; + + // Decide whether to resolve this token as a path. + // Don't resolve if it already has a drive prefix, or starts with '-' + bool resolve = cwd[0] && !has_drive_prefix(tokStart) && tokStart[0] != '-'; + + if (resolve) { + // Write "0://" + if (o + 3 < outMax) { out[o++] = '0'; out[o++] = ':'; out[o++] = '/'; } + int j = 0; + while (cwd[j] && o < outMax - 1) out[o++] = cwd[j++]; + if (o < outMax - 1) out[o++] = '/'; + for (int k = 0; k < tokLen && o < outMax - 1; k++) out[o++] = tokStart[k]; + } else { + for (int k = 0; k < tokLen && o < outMax - 1; k++) out[o++] = tokStart[k]; + } + + p = tokStart + tokLen; + } + out[o] = '\0'; } -static void cmd_clear() { - zenith::print("\033[2J"); // Clear entire screen - zenith::print("\033[H"); // Move cursor to top-left +static void exec_external(const char* cmd, const char* args) { + char path[256]; + + // Resolve arguments against CWD so external programs get full VFS paths + char resolvedArgs[512]; + resolve_args(args, resolvedArgs, sizeof(resolvedArgs)); + const char* finalArgs = resolvedArgs[0] ? resolvedArgs : nullptr; + + // 1. Try 0:/os/.elf + scopy(path, "0:/os/", sizeof(path)); + scat(path, cmd, sizeof(path)); + scat(path, ".elf", sizeof(path)); + if (try_exec(path, finalArgs)) return; + + // 2. Try 0:/games/.elf + scopy(path, "0:/games/", sizeof(path)); + scat(path, cmd, sizeof(path)); + scat(path, ".elf", sizeof(path)); + if (try_exec(path, finalArgs)) return; + + // 3. Try 0://.elf (if cwd is set) + if (cwd[0]) { + scopy(path, "0:/", sizeof(path)); + scat(path, cwd, sizeof(path)); + scat(path, "/", sizeof(path)); + scat(path, cmd, sizeof(path)); + scat(path, ".elf", sizeof(path)); + if (try_exec(path, finalArgs)) return; + } + + // 4. Try 0:/.elf + scopy(path, "0:/", sizeof(path)); + scat(path, cmd, sizeof(path)); + scat(path, ".elf", sizeof(path)); + if (try_exec(path, finalArgs)) return; + + // Not found + zenith::print(cmd); + zenith::print(": command not found\n"); } +// ---- Command dispatch ---- + static void process_command(const char* line) { - // Skip leading spaces line = skip_spaces(line); if (*line == '\0') return; - if (streq(line, "help")) { + // Parse command name and arguments + char cmd[128]; + int i = 0; + while (line[i] && line[i] != ' ' && i < 127) { + cmd[i] = line[i]; + i++; + } + cmd[i] = '\0'; + + const char* args = nullptr; + if (line[i] == ' ') { + args = skip_spaces(line + i); + if (*args == '\0') args = nullptr; + } + + // Builtins + if (streq(cmd, "help")) { cmd_help(); - } else if (streq(line, "info")) { - cmd_info(); - } else if (starts_with(line, "ls ")) { - cmd_ls(line + 3); - } else if (streq(line, "ls")) { - cmd_ls(""); - } else if (starts_with(line, "cd ")) { - cmd_cd(line + 3); - } else if (streq(line, "cd")) { - cmd_cd(""); - } else if (starts_with(line, "man ")) { - cmd_man(line + 4); - } else if (streq(line, "man")) { - cmd_man(""); - } else if (starts_with(line, "cat ")) { - cmd_cat(line + 4); - } else if (streq(line, "cat")) { - cmd_cat(""); - } else if (starts_with(line, "run ")) { - cmd_run(line + 4); - } else if (streq(line, "run")) { - cmd_run(""); - } else if (starts_with(line, "ping ")) { - cmd_ping(line + 5); - } else if (streq(line, "ping")) { - cmd_ping(""); - } else if (starts_with(line, "tcpconnect ")) { - cmd_tcpconnect(line + 11); - } else if (streq(line, "tcpconnect")) { - cmd_tcpconnect(""); - } else if (streq(line, "date")) { - cmd_date(); - } else if (streq(line, "uptime")) { - cmd_uptime(); - } else if (streq(line, "clear")) { - cmd_clear(); - } else if (streq(line, "reset")) { - zenith::print("Rebooting...\n"); - zenith::reset(); - } else if (streq(line, "shutdown")) { - zenith::print("Shutting down...\n"); - zenith::shutdown(); - } else if (streq(line, "exit")) { + } else if (streq(cmd, "ls")) { + cmd_ls(args ? args : ""); + } else if (streq(cmd, "cd")) { + cmd_cd(args ? args : ""); + } else if (streq(cmd, "man")) { + cmd_man(args ? args : ""); + } else if (streq(cmd, "exit")) { zenith::print("Goodbye.\n"); zenith::exit(0); } else { - zenith::print("Unknown command: "); - zenith::print(line); - zenith::print("\nType 'help' for available commands.\n"); + // External command -- pass full argument string + exec_external(cmd, args); } } +// ---- Arrow key scancodes ---- + +static constexpr uint8_t SC_UP = 0x48; +static constexpr uint8_t SC_DOWN = 0x50; +static constexpr uint8_t SC_LEFT = 0x4B; +static constexpr uint8_t SC_RIGHT = 0x4D; + +// ---- Entry point ---- + extern "C" void _start() { zenith::print("\n"); zenith::print(" ZenithOS\n"); @@ -641,28 +435,69 @@ extern "C" void _start() { char line[256]; int pos = 0; + int hist_nav = -1; // -1 = not navigating history prompt(); while (true) { - char c = zenith::getchar(); + if (!zenith::is_key_available()) { + zenith::yield(); + continue; + } - if (c == '\n') { + Zenith::KeyEvent ev; + zenith::getkey(&ev); + + if (!ev.pressed) continue; + + // Arrow keys: ascii == 0, check scancode + if (ev.ascii == 0) { + if (ev.scancode == SC_UP) { + // Navigate to older history entry + int next = hist_nav + 1; + const char* entry = history_get(next); + if (entry) { + hist_nav = next; + replace_line(line, &pos, entry); + } + } else if (ev.scancode == SC_DOWN) { + // Navigate to newer history entry + if (hist_nav > 0) { + hist_nav--; + const char* entry = history_get(hist_nav); + if (entry) { + replace_line(line, &pos, entry); + } + } else if (hist_nav == 0) { + // Back to empty line + hist_nav = -1; + erase_input(pos); + pos = 0; + line[0] = '\0'; + } + } + // Left/Right arrows: ignore for now (no cursor movement within line) + continue; + } + + if (ev.ascii == '\n') { zenith::putchar('\n'); line[pos] = '\0'; + history_add(line); process_command(line); pos = 0; + hist_nav = -1; prompt(); - } else if (c == '\b') { + } else if (ev.ascii == '\b') { if (pos > 0) { pos--; zenith::putchar('\b'); zenith::putchar(' '); zenith::putchar('\b'); } - } else if (c >= ' ' && pos < 255) { - line[pos++] = c; - zenith::putchar(c); + } else if (ev.ascii >= ' ' && pos < 255) { + line[pos++] = ev.ascii; + zenith::putchar(ev.ascii); } } } diff --git a/programs/src/shutdown/main.cpp b/programs/src/shutdown/main.cpp new file mode 100644 index 0000000..9f96a9c --- /dev/null +++ b/programs/src/shutdown/main.cpp @@ -0,0 +1,12 @@ +/* + * main.cpp + * shutdown - Shut down the system + * Copyright (c) 2025-2026 Daniel Hammer +*/ + +#include + +extern "C" void _start() { + zenith::print("Shutting down...\n"); + zenith::shutdown(); +} diff --git a/programs/src/tcpconnect/main.cpp b/programs/src/tcpconnect/main.cpp new file mode 100644 index 0000000..f8b9cbf --- /dev/null +++ b/programs/src/tcpconnect/main.cpp @@ -0,0 +1,194 @@ +/* + * main.cpp + * tcpconnect - Interactive TCP client + * Copyright (c) 2025-2026 Daniel Hammer +*/ + +#include +#include + +using zenith::skip_spaces; + +static void print_int(uint64_t n) { + if (n == 0) { + zenith::putchar('0'); + return; + } + char buf[20]; + int i = 0; + while (n > 0) { + buf[i++] = '0' + (n % 10); + n /= 10; + } + for (int j = i - 1; j >= 0; j--) { + zenith::putchar(buf[j]); + } +} + +static bool parse_ip(const char* s, uint32_t* out) { + uint32_t octets[4]; + int idx = 0; + uint32_t val = 0; + bool hasDigit = false; + + for (int i = 0; ; i++) { + char c = s[i]; + if (c >= '0' && c <= '9') { + val = val * 10 + (c - '0'); + if (val > 255) return false; + hasDigit = true; + } else if (c == '.' || c == '\0') { + if (!hasDigit || idx >= 4) return false; + octets[idx++] = val; + val = 0; + hasDigit = false; + if (c == '\0') break; + } else { + return false; + } + } + + if (idx != 4) return false; + *out = octets[0] | (octets[1] << 8) | (octets[2] << 16) | (octets[3] << 24); + return true; +} + +static void print_ip(uint32_t ip) { + print_int(ip & 0xFF); + zenith::putchar('.'); + print_int((ip >> 8) & 0xFF); + zenith::putchar('.'); + print_int((ip >> 16) & 0xFF); + zenith::putchar('.'); + print_int((ip >> 24) & 0xFF); +} + +static bool parse_uint16(const char* s, uint16_t* out) { + uint32_t val = 0; + if (*s == '\0') return false; + while (*s) { + if (*s < '0' || *s > '9') return false; + val = val * 10 + (*s - '0'); + if (val > 65535) return false; + s++; + } + *out = (uint16_t)val; + return true; +} + +extern "C" void _start() { + char args[256]; + int len = zenith::getargs(args, sizeof(args)); + + if (len <= 0 || args[0] == '\0') { + zenith::print("Usage: tcpconnect \n"); + zenith::exit(1); + } + + // Parse IP address (up to first space) + char ipStr[32]; + int i = 0; + while (args[i] && args[i] != ' ' && i < 31) { + ipStr[i] = args[i]; + i++; + } + ipStr[i] = '\0'; + + uint32_t ip; + if (!parse_ip(ipStr, &ip)) { + zenith::print("Invalid IP address: "); + zenith::print(ipStr); + zenith::putchar('\n'); + zenith::exit(1); + } + + // Parse port + const char* portStr = skip_spaces(args + i); + if (*portStr == '\0') { + zenith::print("Usage: tcpconnect \n"); + zenith::exit(1); + } + uint16_t port; + if (!parse_uint16(portStr, &port)) { + zenith::print("Invalid port: "); + zenith::print(portStr); + zenith::putchar('\n'); + zenith::exit(1); + } + + // Create socket + int fd = zenith::socket(Zenith::SOCK_TCP); + if (fd < 0) { + zenith::print("Error: failed to create socket\n"); + zenith::exit(1); + } + + zenith::print("Connecting to "); + print_ip(ip); + zenith::putchar(':'); + print_int(port); + zenith::print("...\n"); + + if (zenith::connect(fd, ip, port) < 0) { + zenith::print("Error: connection failed\n"); + zenith::closesocket(fd); + zenith::exit(1); + } + + zenith::print("Connected! Type to send, Ctrl+Q to disconnect.\n"); + + // Interactive send/receive loop + char sendBuf[256]; + int sendPos = 0; + uint8_t recvBuf[512]; + + while (true) { + // Poll for received data (non-blocking) + int r = zenith::recv(fd, recvBuf, sizeof(recvBuf) - 1); + if (r < 0) { + zenith::print("\nConnection closed by remote.\n"); + break; + } + if (r > 0) { + recvBuf[r] = '\0'; + zenith::print((const char*)recvBuf); + } + + // Poll keyboard + if (zenith::is_key_available()) { + Zenith::KeyEvent ev; + zenith::getkey(&ev); + + if (!ev.pressed) continue; + + // Ctrl+Q to quit + if (ev.ctrl && (ev.ascii == 'q' || ev.ascii == 'Q')) { + zenith::print("\nDisconnecting...\n"); + break; + } + + if (ev.ascii == '\n') { + sendBuf[sendPos++] = '\n'; + zenith::putchar('\n'); + zenith::send(fd, sendBuf, sendPos); + sendPos = 0; + } else if (ev.ascii == '\b') { + if (sendPos > 0) { + sendPos--; + zenith::putchar('\b'); + zenith::putchar(' '); + zenith::putchar('\b'); + } + } else if (ev.ascii >= ' ' && sendPos < 254) { + sendBuf[sendPos++] = ev.ascii; + zenith::putchar(ev.ascii); + } + } else { + // No key and no data -- yield to avoid busy-spinning + zenith::yield(); + } + } + + zenith::closesocket(fd); + zenith::exit(0); +} diff --git a/programs/src/uptime/main.cpp b/programs/src/uptime/main.cpp new file mode 100644 index 0000000..da79774 --- /dev/null +++ b/programs/src/uptime/main.cpp @@ -0,0 +1,40 @@ +/* + * main.cpp + * uptime - Show system uptime + * Copyright (c) 2025-2026 Daniel Hammer +*/ + +#include + +static void print_int(uint64_t n) { + if (n == 0) { + zenith::putchar('0'); + return; + } + char buf[20]; + int i = 0; + while (n > 0) { + buf[i++] = '0' + (n % 10); + n /= 10; + } + for (int j = i - 1; j >= 0; j--) { + zenith::putchar(buf[j]); + } +} + +extern "C" void _start() { + uint64_t ms = zenith::get_milliseconds(); + uint64_t secs = ms / 1000; + uint64_t mins = secs / 60; + secs %= 60; + ms %= 1000; + + zenith::print("Uptime: "); + print_int(mins); + zenith::print("m "); + print_int(secs); + zenith::print("s "); + print_int(ms); + zenith::print("ms\n"); + zenith::exit(0); +} diff --git a/programs/www/index.html b/programs/www/index.html new file mode 100644 index 0000000..8fa3738 --- /dev/null +++ b/programs/www/index.html @@ -0,0 +1,31 @@ + + + + +ZenithOS + + + +
+ +

+ZenithOS +

+ +
+ +

Browse

+ + + +
+ +

+ZenithOS · httpd/1.0 +

+ +
+ + diff --git a/ramdisk.tar b/ramdisk.tar index a3dd819..d9cb287 100644 Binary files a/ramdisk.tar and b/ramdisk.tar differ