From 550d4d2a190023fbb450ccacfb9f837e85b67b71 Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Wed, 1 Apr 2026 21:15:27 +0200 Subject: [PATCH] feat: support for IPP printing + test page --- .codex | 0 GNUmakefile | 3 + kernel/src/Fs/Boot.cpp | 2 +- kernel/src/Fs/Ramdisk.cpp | 100 ++ kernel/src/Fs/Ramdisk.hpp | 1 + kernel/src/Net/Tcp.cpp | 112 +- programs/GNUmakefile | 23 +- programs/include/http/http.hpp | 95 +- programs/include/montauk/syscall.h | 3 + programs/include/print/print.hpp | 1739 +++++++++++++++++++ programs/man/printctl.1 | 32 + programs/man/printd.1 | 12 + programs/src/desktop/apps/app_settings.cpp | 11 +- programs/src/init/main.cpp | 1 + programs/src/printctl/Makefile | 77 + programs/src/printctl/main.cpp | 360 ++++ programs/src/printd/Makefile | 79 + programs/src/printd/font_data.cpp | 1 + programs/src/printd/main.cpp | 273 +++ programs/src/printd/stb_truetype_impl.cpp | 32 + programs/src/printd/test_page_jpeg.cpp | 311 ++++ programs/src/printd/test_page_jpeg.hpp | 5 + programs/src/printers/Makefile | 79 + programs/src/printers/font_data.cpp | 1 + programs/src/printers/main.cpp | 789 +++++++++ programs/src/printers/manifest.toml | 8 + programs/src/printers/stb_truetype_impl.cpp | 33 + scripts/install_apps.sh | 1 + 28 files changed, 4121 insertions(+), 62 deletions(-) create mode 100644 .codex create mode 100644 programs/include/print/print.hpp create mode 100644 programs/man/printctl.1 create mode 100644 programs/man/printd.1 create mode 100644 programs/src/printctl/Makefile create mode 100644 programs/src/printctl/main.cpp create mode 100644 programs/src/printd/Makefile create mode 100644 programs/src/printd/font_data.cpp create mode 100644 programs/src/printd/main.cpp create mode 100644 programs/src/printd/stb_truetype_impl.cpp create mode 100644 programs/src/printd/test_page_jpeg.cpp create mode 100644 programs/src/printd/test_page_jpeg.hpp create mode 100644 programs/src/printers/Makefile create mode 100644 programs/src/printers/font_data.cpp create mode 100644 programs/src/printers/main.cpp create mode 100644 programs/src/printers/manifest.toml create mode 100644 programs/src/printers/stb_truetype_impl.cpp diff --git a/.codex b/.codex new file mode 100644 index 0000000..e69de29 diff --git a/GNUmakefile b/GNUmakefile index bb82296..a23668e 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -174,6 +174,9 @@ kernel-deps: kernel: kernel-deps $(MAKE) -C kernel +.PHONY: programs +programs: + $(MAKE) -C programs .PHONY: ramdisk ramdisk: limine/limine kernel programs diff --git a/kernel/src/Fs/Boot.cpp b/kernel/src/Fs/Boot.cpp index 17a6691..847526c 100644 --- a/kernel/src/Fs/Boot.cpp +++ b/kernel/src/Fs/Boot.cpp @@ -64,7 +64,7 @@ namespace Fs { Ramdisk::Create, Ramdisk::Delete, Ramdisk::Mkdir, - nullptr, + Ramdisk::Rename, }; } diff --git a/kernel/src/Fs/Ramdisk.cpp b/kernel/src/Fs/Ramdisk.cpp index 4f23d51..d932b25 100644 --- a/kernel/src/Fs/Ramdisk.cpp +++ b/kernel/src/Fs/Ramdisk.cpp @@ -47,6 +47,36 @@ namespace Fs::Ramdisk { return true; } + static int NormalizePath(const char* path, char* out, int outMax) { + if (path == nullptr || out == nullptr || outMax <= 1) return -1; + if (path[0] == '/') path++; + + int len = 0; + while (path[len] != '\0' && len < outMax - 1) { + out[len] = path[len]; + len++; + } + out[len] = '\0'; + + while (len > 1 && out[len - 1] == '/') { + out[--len] = '\0'; + } + return len; + } + + static int FindEntryByPath(const char* path) { + char normalized[MaxNameLen]; + int normLen = NormalizePath(path, normalized, MaxNameLen); + if (normLen < 0) return -1; + + for (int i = 0; i < fileCount; i++) { + char entryPath[MaxNameLen]; + NormalizePath(fileTable[i].name, entryPath, MaxNameLen); + if (StrEqual(entryPath, normalized)) return i; + } + return -1; + } + void Initialize(void* moduleData, uint64_t moduleSize) { Kt::KernelLogStream(Kt::OK, "Ramdisk") << "Parsing USTAR archive (" << moduleSize << " bytes)"; @@ -405,6 +435,76 @@ namespace Fs::Ramdisk { return 0; } + int Rename(const char* oldPath, const char* newPath) { + char oldNorm[MaxNameLen]; + char newNorm[MaxNameLen]; + int oldLen = NormalizePath(oldPath, oldNorm, MaxNameLen); + int newLen = NormalizePath(newPath, newNorm, MaxNameLen); + if (oldLen <= 0 || newLen <= 0) return -1; + + int src = FindEntryByPath(oldNorm); + if (src < 0) return -1; + + int dst = FindEntryByPath(newNorm); + if (dst >= 0 && dst != src) { + if (Delete(newNorm) < 0) return -1; + if (dst < src) src--; + } + + const bool isDirectory = fileTable[src].isDirectory; + + if (!isDirectory) { + if (newLen >= MaxNameLen) return -1; + for (int i = 0; i <= newLen; i++) fileTable[src].name[i] = newNorm[i]; + return 0; + } + + char oldPrefix[MaxNameLen]; + if (oldLen + 1 >= MaxNameLen || newLen + 1 >= MaxNameLen) return -1; + + for (int i = 0; i < oldLen; i++) oldPrefix[i] = oldNorm[i]; + oldPrefix[oldLen] = '/'; + oldPrefix[oldLen + 1] = '\0'; + + for (int i = 0; i < fileCount; i++) { + char normalized[MaxNameLen]; + int normalizedLen = NormalizePath(fileTable[i].name, normalized, MaxNameLen); + if (normalizedLen < 0) return -1; + + bool exactMatch = StrEqual(normalized, oldNorm); + bool childMatch = StartsWith(fileTable[i].name, oldPrefix); + if (!exactMatch && !childMatch) continue; + + char updated[MaxNameLen]; + if (exactMatch) { + for (int j = 0; j <= newLen; j++) updated[j] = newNorm[j]; + } else { + int suffixLen = StrLen(fileTable[i].name) - (oldLen + 1); + if (newLen + 1 + suffixLen >= MaxNameLen) return -1; + + int pos = 0; + for (int j = 0; j < newLen; j++) updated[pos++] = newNorm[j]; + updated[pos++] = '/'; + const char* suffix = fileTable[i].name + oldLen + 1; + while (*suffix && pos < MaxNameLen - 1) updated[pos++] = *suffix++; + updated[pos] = '\0'; + } + + int pos = 0; + while (updated[pos] != '\0' && pos < MaxNameLen - 1) { + fileTable[i].name[pos] = updated[pos]; + pos++; + } + if (fileTable[i].isDirectory) { + if (pos >= MaxNameLen - 1) return -1; + fileTable[i].name[pos++] = '/'; + } + fileTable[i].name[pos] = '\0'; + } + + return 0; + } + int GetFileCount() { return fileCount; } diff --git a/kernel/src/Fs/Ramdisk.hpp b/kernel/src/Fs/Ramdisk.hpp index 8d54448..49fa63d 100644 --- a/kernel/src/Fs/Ramdisk.hpp +++ b/kernel/src/Fs/Ramdisk.hpp @@ -34,6 +34,7 @@ namespace Fs::Ramdisk { int ReadDir(const char* path, const char** outNames, int maxEntries); int Delete(const char* path); int Mkdir(const char* path); + int Rename(const char* oldPath, const char* newPath); int GetFileCount(); } diff --git a/kernel/src/Net/Tcp.cpp b/kernel/src/Net/Tcp.cpp index d325919..bcd57fb 100644 --- a/kernel/src/Net/Tcp.cpp +++ b/kernel/src/Net/Tcp.cpp @@ -553,11 +553,6 @@ namespace Net::Tcp { return -1; } - // Phase 1: Send data segments with interrupts disabled (lock-safe) - uint64_t flags; - asm volatile("pushfq; pop %0; cli" : "=r"(flags) :: "memory"); - conn->Lock.Acquire(); - constexpr uint16_t MSS = 1460; uint16_t sent = 0; @@ -567,6 +562,21 @@ namespace Net::Tcp { segLen = MSS; } + uint32_t segSeq = 0; + uint32_t expectedAck = 0; + uint64_t flags; + asm volatile("pushfq; pop %0; cli" : "=r"(flags) :: "memory"); + conn->Lock.Acquire(); + + if (conn->CurrentState != State::Established) { + conn->Lock.Release(); + asm volatile("push %0; popfq" :: "r"(flags) : "memory"); + return sent > 0 ? sent : -1; + } + + segSeq = conn->SendNext; + expectedAck = segSeq + segLen; + bool ok = SendSegment(conn, FLAG_ACK | FLAG_PSH, data + sent, segLen); if (!ok) { conn->Lock.Release(); @@ -574,9 +584,8 @@ namespace Net::Tcp { return sent > 0 ? sent : -1; } - conn->SendNext += segLen; + conn->SendNext = expectedAck; - // Store for retransmission if (segLen <= sizeof(conn->RetransmitBuffer)) { memcpy(conn->RetransmitBuffer, data + sent, segLen); conn->RetransmitLen = segLen; @@ -584,44 +593,69 @@ namespace Net::Tcp { conn->RetransmitCount = 0; } - sent += segLen; - } + conn->Lock.Release(); + asm volatile("push %0; popfq" :: "r"(flags) : "memory"); - conn->Lock.Release(); - asm volatile("push %0; popfq" :: "r"(flags) : "memory"); - - // Phase 2: Wait for ACK without holding the lock (interrupts enabled) - uint64_t startTime = Timekeeping::GetMilliseconds(); - while (conn->SendUnack != conn->SendNext) { - uint64_t now = Timekeeping::GetMilliseconds(); - if ((now - startTime) > (RETRANSMIT_TIMEOUT_MS * MAX_RETRANSMITS)) { - break; - } - if ((now - conn->RetransmitTime) > RETRANSMIT_TIMEOUT_MS && conn->RetransmitLen > 0) { - conn->RetransmitCount++; - if (conn->RetransmitCount > MAX_RETRANSMITS) { - break; - } - // Retransmit with brief interrupt-disabled window + while (true) { asm volatile("pushfq; pop %0; cli" : "=r"(flags) :: "memory"); conn->Lock.Acquire(); - uint32_t savedNext = conn->SendNext; - conn->SendNext = conn->SendUnack; - SendSegment(conn, FLAG_ACK | FLAG_PSH, - conn->RetransmitBuffer, conn->RetransmitLen); - conn->SendNext = savedNext; - conn->RetransmitTime = Timekeeping::GetMilliseconds(); + + if (conn->SendUnack >= expectedAck) { + conn->RetransmitLen = 0; + conn->Lock.Release(); + asm volatile("push %0; popfq" :: "r"(flags) : "memory"); + sent += segLen; + break; + } + + if (conn->CurrentState != State::Established) { + conn->SendNext = conn->SendUnack; + conn->RetransmitLen = 0; + conn->Lock.Release(); + asm volatile("push %0; popfq" :: "r"(flags) : "memory"); + return sent > 0 ? sent : -1; + } + + uint64_t now = Timekeeping::GetMilliseconds(); + bool shouldRetransmit = + conn->RetransmitLen == segLen && + (now - conn->RetransmitTime) > RETRANSMIT_TIMEOUT_MS; + if (shouldRetransmit) { + conn->RetransmitCount++; + if (conn->RetransmitCount > MAX_RETRANSMITS) { + conn->SendNext = conn->SendUnack; + conn->RetransmitLen = 0; + conn->Lock.Release(); + asm volatile("push %0; popfq" :: "r"(flags) : "memory"); + return sent > 0 ? sent : -1; + } + + uint32_t savedNext = conn->SendNext; + conn->SendNext = segSeq; + bool retryOk = SendSegment(conn, FLAG_ACK | FLAG_PSH, + conn->RetransmitBuffer, conn->RetransmitLen); + conn->SendNext = savedNext; + conn->RetransmitTime = now; + if (!retryOk) { + conn->SendNext = conn->SendUnack; + conn->RetransmitLen = 0; + conn->Lock.Release(); + asm volatile("push %0; popfq" :: "r"(flags) : "memory"); + return sent > 0 ? sent : -1; + } + } + + uint64_t waitMs = 10; + if (conn->RetransmitLen == segLen && now <= conn->RetransmitTime + RETRANSMIT_TIMEOUT_MS) { + waitMs = (conn->RetransmitTime + RETRANSMIT_TIMEOUT_MS) - now; + if (waitMs == 0) waitMs = 1; + if (waitMs > 10) waitMs = 10; + } + conn->Lock.Release(); asm volatile("push %0; popfq" :: "r"(flags) : "memory"); - continue; + Sched::BlockOnObject(conn, waitMs); } - - uint64_t waitMs = 10; - if (conn->RetransmitLen > 0 && now <= conn->RetransmitTime + RETRANSMIT_TIMEOUT_MS) { - waitMs = (conn->RetransmitTime + RETRANSMIT_TIMEOUT_MS) - now; - if (waitMs == 0) waitMs = 1; - } - Sched::BlockOnObject(conn, waitMs); } return sent; diff --git a/programs/GNUmakefile b/programs/GNUmakefile index 9cb32fa..d418b90 100644 --- a/programs/GNUmakefile +++ b/programs/GNUmakefile @@ -59,7 +59,7 @@ BINDIR := bin PROGRAMS := $(notdir $(wildcard src/*)) # Programs with custom Makefiles (built separately). -CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator desktop login shell rpgdemo paint tcc lua screenshot texteditor mandelbrot +CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator desktop login shell rpgdemo paint tcc lua screenshot texteditor mandelbrot printers printd printctl SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS)) # Build targets: system programs go to bin/os/, apps go to bin/apps//. @@ -81,9 +81,9 @@ CA_CERTS := $(BINDIR)/etc/ca-certificates.crt # Common shared assets (wallpapers, etc.) COMMONKEEP := $(BINDIR)/common/.keep -.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator login desktop shell rpgdemo paint tcc lua screenshot texteditor mandelbrot icons fonts bearssl libc tls libjpeg libjpegwrite install-apps +.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator login desktop shell rpgdemo paint tcc lua screenshot texteditor mandelbrot printers printd printctl icons fonts bearssl libc tls libjpeg libjpegwrite install-apps -all: bearssl libc libjpeg libjpegwrite tls $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator doom rpgdemo paint tcc lua screenshot texteditor mandelbrot login desktop shell icons fonts install-apps $(MANDST) $(WWWDST) $(CA_CERTS) $(COMMONKEEP) +all: bearssl libc libjpeg libjpegwrite tls $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator doom rpgdemo paint tcc lua screenshot texteditor mandelbrot printers printd printctl login desktop shell icons fonts install-apps $(MANDST) $(WWWDST) $(CA_CERTS) $(COMMONKEEP) # Build BearSSL static library (cross-compiled for freestanding x86_64). BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc) @@ -192,6 +192,10 @@ procmgr: libc calculator: libc $(MAKE) -C src/calculator +# Build printers standalone GUI tool (depends on libc, bearssl, and tls). +printers: bearssl libc tls + $(MAKE) -C src/printers + # Build login screen via its own Makefile (depends on bearssl and libc). login: bearssl libc $(MAKE) -C src/login @@ -249,8 +253,16 @@ tcc: libc lua: libc $(MAKE) -C src/lua +# Build print spooler daemon (depends on libc, tls, and bearssl). +printd: bearssl libc tls libjpegwrite + $(MAKE) -C src/printd + +# Build print control utility (depends on libc, tls, and bearssl). +printctl: bearssl libc tls + $(MAKE) -C src/printctl + # Install app bundles (manifests, icons, data files) into bin/apps//. -install-apps: doom rpgdemo paint spreadsheet wordprocessor weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator screenshot texteditor mandelbrot +install-apps: doom rpgdemo paint spreadsheet wordprocessor weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator screenshot texteditor mandelbrot printers ../scripts/install_apps.sh # Copy man pages into bin/man/ so mkramdisk.sh picks them up. @@ -309,8 +321,11 @@ clean: $(MAKE) -C src/klog clean $(MAKE) -C src/procmgr clean $(MAKE) -C src/calculator clean + $(MAKE) -C src/printers clean $(MAKE) -C src/paint clean $(MAKE) -C src/rpgdemo clean $(MAKE) -C src/screenshot clean $(MAKE) -C src/mandelbrot clean $(MAKE) -C src/tcc clean + $(MAKE) -C src/printd clean + $(MAKE) -C src/printctl clean diff --git a/programs/include/http/http.hpp b/programs/include/http/http.hpp index 8a10eb5..e8f87ec 100644 --- a/programs/include/http/http.hpp +++ b/programs/include/http/http.hpp @@ -27,7 +27,64 @@ struct Response { int raw_len; }; +inline const char* find_header_block_end(const char* start, const char* end) { + if (!start || !end || start >= end) return nullptr; + + for (const char* s = start; s < end - 3; s++) { + if (s[0] == '\r' && s[1] == '\n' && s[2] == '\r' && s[3] == '\n') + return s + 4; + } + for (const char* s = start; s < end - 1; s++) { + if (s[0] == '\n' && s[1] == '\n') + return s + 2; + } + return nullptr; +} + +inline int parse_status_code(const char* start, const char* end) { + if (!start || !end || end - start < 12) return -1; + + const char* p = start; + while (p < end && *p && *p != ' ') p++; + if (p >= end || *p != ' ') return -1; + p++; + + int code = 0; + int digits = 0; + while (digits < 3 && p < end && *p >= '0' && *p <= '9') { + code = code * 10 + (*p - '0'); + p++; + digits++; + } + return digits == 3 ? code : -1; +} + +inline const char* find_final_response_start(const char* buf, int len) { + if (!buf || len <= 0) return nullptr; + + const char* start = buf; + const char* end = buf + len; + for (;;) { + int code = parse_status_code(start, end); + if (code < 0) return nullptr; + if (code < 100 || code >= 200) return start; + + const char* next = find_header_block_end(start, end); + if (!next || next >= end) return nullptr; + if (end - next < 5) return nullptr; + if (!(next[0] == 'H' && next[1] == 'T' && next[2] == 'T' && next[3] == 'P' && next[4] == '/')) + return nullptr; + start = next; + } +} + +inline const char* skip_informational_responses(const char* buf, int len) { + const char* start = find_final_response_start(buf, len); + return start ? start : buf; +} + // Parse raw HTTP response in-place. Sets pointers into buf (does not copy). +// Skips leading informational 1xx responses such as "100 Continue". // Returns status code, or -1 if unparseable. inline int parse_response(char* buf, int len, Response* out) { out->raw = buf; @@ -38,21 +95,18 @@ inline int parse_response(char* buf, int len, Response* out) { out->body = nullptr; out->body_len = 0; - if (len < 12) return -1; // "HTTP/1.x NNN" + const char* start = find_final_response_start(buf, len); + const char* end = buf + len; + if (!start || end - start < 12) return -1; // "HTTP/1.x NNN" // Parse status code from "HTTP/1.x NNN" - const char* p = buf; - while (*p && *p != ' ') p++; - if (*p != ' ') return -1; - p++; - int code = 0; - for (int i = 0; i < 3 && *p >= '0' && *p <= '9'; i++, p++) - code = code * 10 + (*p - '0'); + int code = parse_status_code(start, end); + if (code < 0) return -1; out->status = code; // Headers start after the status line - const char* hdr_start = buf; - while (hdr_start < buf + len - 1) { + const char* hdr_start = start; + while (hdr_start < end - 1) { if (*hdr_start == '\r' && *(hdr_start + 1) == '\n') { hdr_start += 2; break; } if (*hdr_start == '\n') { hdr_start++; break; } hdr_start++; @@ -60,14 +114,21 @@ inline int parse_response(char* buf, int len, Response* out) { out->headers = hdr_start; // Find \r\n\r\n boundary between headers and body - for (const char* s = hdr_start; s < buf + len - 3; s++) { - if (s[0] == '\r' && s[1] == '\n' && s[2] == '\r' && s[3] == '\n') { - out->headers_len = (int)(s - hdr_start); - out->body = s + 4; - out->body_len = len - (int)(out->body - buf); - return code; - } + const char* body = find_header_block_end(hdr_start, end); + if (body) { + if (body >= hdr_start + 4 && + body[-4] == '\r' && body[-3] == '\n' && body[-2] == '\r' && body[-1] == '\n') + out->headers_len = (int)((body - 4) - hdr_start); + else if (body >= hdr_start + 2 && + body[-2] == '\n' && body[-1] == '\n') + out->headers_len = (int)((body - 2) - hdr_start); + else + out->headers_len = (int)(body - hdr_start); + out->body = body; + out->body_len = len - (int)(out->body - buf); + return code; } + // No body separator found — entire remainder is headers out->headers_len = len - (int)(hdr_start - buf); return code; diff --git a/programs/include/montauk/syscall.h b/programs/include/montauk/syscall.h index 22e849c..fbccf31 100644 --- a/programs/include/montauk/syscall.h +++ b/programs/include/montauk/syscall.h @@ -155,6 +155,9 @@ namespace montauk { inline int fmkdir(const char* path) { return (int)syscall1(Montauk::SYS_FMKDIR, (uint64_t)path); } + inline int frename(const char* oldPath, const char* newPath) { + return (int)syscall2(Montauk::SYS_FRENAME, (uint64_t)oldPath, (uint64_t)newPath); + } inline int drivelist(int* outDrives, int max) { return (int)syscall2(Montauk::SYS_DRIVELIST, (uint64_t)outDrives, (uint64_t)max); } diff --git a/programs/include/print/print.hpp b/programs/include/print/print.hpp new file mode 100644 index 0000000..3fd9113 --- /dev/null +++ b/programs/include/print/print.hpp @@ -0,0 +1,1739 @@ +/* + * print.hpp + * Shared helpers for the MontaukOS userspace print stack + * Copyright (c) 2026 Daniel Hammer + */ + +#pragma once + +#include +#include +#include +#include + +extern "C" { +#include +#include +#include +#include +#include +} + +namespace print { + +static constexpr const char* CONFIG_DIR = "0:/config/printing"; +static constexpr const char* DEFAULT_PRINTER_PATH = "0:/config/printing/default-printer-uri"; +static constexpr const char* SPOOL_ROOT = "0:/spool/print"; +static constexpr const char* SPOOL_QUEUE_DIR = "0:/spool/print/queue"; +static constexpr const char* SPOOL_ACTIVE_DIR = "0:/spool/print/active"; +static constexpr const char* SPOOL_DONE_DIR = "0:/spool/print/done"; +static constexpr const char* SPOOL_FAILED_DIR = "0:/spool/print/failed"; +static constexpr const char* SPOOL_DOCS_DIR = "0:/spool/print/docs"; +static constexpr const char* SPOOL_TMP_DIR = "0:/spool/print/tmp"; +static constexpr const char* SPOOL_DAEMON_PID_PATH = "0:/spool/print/daemon.pid"; +static constexpr const char* PRINTD_BINARY = "0:/os/printd.elf"; +static constexpr int MAX_PATH_LEN = 256; +static constexpr int MAX_JOB_NAME_LEN = 128; +static constexpr int MAX_STATUS_LEN = 192; +static constexpr int MAX_DEBUG_LEN = 320; +static constexpr int MAX_DOC_BYTES = 4 * 1024 * 1024; +static constexpr int HTTP_RESPONSE_MAX = 64 * 1024; +static constexpr uint64_t JOB_WAIT_SLICE_MS = 250; +static constexpr uint64_t DEFAULT_WAIT_TIMEOUT_MS = 120000; + +struct JobMeta { + char id[48]; + char state[16]; + char source_kind[16]; + char job_name[MAX_JOB_NAME_LEN]; + char user_name[64]; + char printer_uri[MAX_PATH_LEN]; + char doc_path[MAX_PATH_LEN]; + char doc_format[64]; + char source_name[128]; + char created_at[32]; + char updated_at[32]; + char status_message[MAX_STATUS_LEN]; + char debug_info[MAX_DEBUG_LEN]; + int remote_job_id; + int size_bytes; +}; + +struct IppUri { + bool use_tls; + char host[128]; + uint16_t port; + char path[128]; + char normalized[MAX_PATH_LEN]; + uint32_t ip; +}; + +struct IppCapabilities { + bool ok; + bool supports_pdf; + bool supports_text; + bool supports_jpeg; + bool use_tls; + int http_status; + uint16_t ipp_status; + int request_id; + uint32_t resolved_ip; + uint16_t port; + char host[128]; + char path[128]; + char printer_name[128]; + char status_message[128]; + char supported_formats[192]; + char default_format[64]; + char preferred_format[64]; +}; + +struct IppPrintResult { + bool ok; + bool use_tls; + int http_status; + uint16_t ipp_status; + int request_id; + int job_id; + uint32_t resolved_ip; + uint16_t port; + char host[128]; + char path[128]; + char document_format[64]; + char job_uri[MAX_PATH_LEN]; + char status_message[128]; +}; + +struct ArgList { + int count; + char* argv[24]; + char storage[512]; +}; + +struct ByteBuilder { + uint8_t* data; + int len; + int cap; +}; + +inline void zero_job(JobMeta* job) { + if (job) memset(job, 0, sizeof(*job)); +} + +inline void safe_copy(char* dst, int dst_len, const char* src) { + if (dst == nullptr || dst_len <= 0) return; + if (src == nullptr) { + dst[0] = '\0'; + return; + } + snprintf(dst, (size_t)dst_len, "%s", src); +} + +inline void format_ipv4(char* out, int out_len, uint32_t ip) { + if (out == nullptr || out_len <= 0) return; + snprintf(out, (size_t)out_len, "%u.%u.%u.%u", + (unsigned)(ip & 0xFFu), + (unsigned)((ip >> 8) & 0xFFu), + (unsigned)((ip >> 16) & 0xFFu), + (unsigned)((ip >> 24) & 0xFFu)); +} + +inline void trim_line(char* s) { + if (s == nullptr) return; + int len = (int)strlen(s); + while (len > 0) { + char c = s[len - 1]; + if (c == '\r' || c == '\n' || c == ' ' || c == '\t') { + s[--len] = '\0'; + continue; + } + break; + } +} + +inline void sanitize_field(char* s) { + if (s == nullptr) return; + for (int i = 0; s[i] != '\0'; i++) { + if (s[i] == '\r' || s[i] == '\n') s[i] = ' '; + } +} + +inline void now_string(char* out, int out_len) { + if (out == nullptr || out_len <= 0) return; + Montauk::DateTime dt = {}; + montauk::gettime(&dt); + snprintf(out, (size_t)out_len, "%04d-%02d-%02d %02d:%02d:%02d", + (int)dt.Year, (int)dt.Month, (int)dt.Day, + (int)dt.Hour, (int)dt.Minute, (int)dt.Second); +} + +inline bool file_exists(const char* path) { + struct stat st = {}; + return path != nullptr && stat(path, &st) == 0; +} + +inline bool dir_exists(const char* path) { + struct stat st = {}; + return path != nullptr && stat(path, &st) == 0 && S_ISDIR(st.st_mode); +} + +inline bool ensure_dir(const char* path) { + if (path == nullptr || *path == '\0') return false; + if (mkdir(path, 0755) == 0) return true; + return dir_exists(path); +} + +inline bool ensure_spool_dirs() { + return ensure_dir("0:/config") + && ensure_dir(CONFIG_DIR) + && ensure_dir("0:/spool") + && ensure_dir(SPOOL_ROOT) + && ensure_dir(SPOOL_QUEUE_DIR) + && ensure_dir(SPOOL_ACTIVE_DIR) + && ensure_dir(SPOOL_DONE_DIR) + && ensure_dir(SPOOL_FAILED_DIR) + && ensure_dir(SPOOL_DOCS_DIR) + && ensure_dir(SPOOL_TMP_DIR); +} + +inline bool path_join(char* out, int out_len, const char* a, const char* b) { + if (out == nullptr || out_len <= 0 || a == nullptr || b == nullptr) return false; + if (*a == '\0') { + safe_copy(out, out_len, b); + return true; + } + size_t alen = strlen(a); + if (alen > 0 && a[alen - 1] == '/') + snprintf(out, (size_t)out_len, "%s%s", a, b); + else + snprintf(out, (size_t)out_len, "%s/%s", a, b); + return true; +} + +inline const char* basename_ptr(const char* path) { + if (path == nullptr) return ""; + const char* last = path; + for (const char* p = path; *p; p++) { + if (*p == '/') last = p + 1; + } + return last; +} + +inline void make_job_file_path(const char* dir, const char* id, char* out, int out_len) { + char leaf[64]; + snprintf(leaf, sizeof(leaf), "%s.job", id); + path_join(out, out_len, dir, leaf); +} + +inline void make_doc_file_path(const char* id, char* out, int out_len) { + char leaf[64]; + snprintf(leaf, sizeof(leaf), "%s.bin", id); + path_join(out, out_len, SPOOL_DOCS_DIR, leaf); +} + +inline bool read_text_file_line(const char* path, char* out, int out_len) { + if (path == nullptr || out == nullptr || out_len <= 0) return false; + FILE* f = fopen(path, "rb"); + if (!f) return false; + out[0] = '\0'; + bool ok = fgets(out, out_len, f) != nullptr; + fclose(f); + if (ok) trim_line(out); + return ok; +} + +inline bool write_text_file_atomic(const char* path, const char* text) { + if (path == nullptr || text == nullptr) return false; + char tmp[MAX_PATH_LEN]; + snprintf(tmp, sizeof(tmp), "%s.tmp", path); + FILE* f = fopen(tmp, "wb"); + if (!f) return false; + size_t want = strlen(text); + bool ok = fwrite(text, 1, want, f) == want; + fclose(f); + if (!ok) { + remove(tmp); + return false; + } + if (rename(tmp, path) != 0) { + remove(tmp); + return false; + } + return true; +} + +inline bool read_file_bytes(const char* path, uint8_t** out_data, int* out_len, char* err, int err_len) { + if (out_data) *out_data = nullptr; + if (out_len) *out_len = 0; + if (path == nullptr) { + safe_copy(err, err_len, "missing path"); + return false; + } + + struct stat st = {}; + if (stat(path, &st) != 0 || !S_ISREG(st.st_mode)) { + safe_copy(err, err_len, "file not found"); + return false; + } + if ((int)st.st_size < 0 || st.st_size > MAX_DOC_BYTES) { + safe_copy(err, err_len, "document is too large"); + return false; + } + + FILE* f = fopen(path, "rb"); + if (!f) { + safe_copy(err, err_len, "failed to open file"); + return false; + } + + int size = (int)st.st_size; + uint8_t* data = (uint8_t*)malloc((size_t)(size > 0 ? size : 1)); + if (!data) { + fclose(f); + safe_copy(err, err_len, "out of memory"); + return false; + } + + bool ok = true; + if (size > 0 && fread(data, 1, (size_t)size, f) != (size_t)size) { + ok = false; + } + fclose(f); + + if (!ok) { + free(data); + safe_copy(err, err_len, "failed to read file"); + return false; + } + + if (out_data) *out_data = data; + if (out_len) *out_len = size; + return true; +} + +inline bool write_file_atomic(const char* path, const uint8_t* data, int len, char* err, int err_len) { + if (path == nullptr || len < 0) { + safe_copy(err, err_len, "invalid write parameters"); + return false; + } + + char tmp[MAX_PATH_LEN]; + snprintf(tmp, sizeof(tmp), "%s.tmp", path); + FILE* f = fopen(tmp, "wb"); + if (!f) { + safe_copy(err, err_len, "failed to create temporary file"); + return false; + } + + bool ok = true; + if (len > 0 && fwrite(data, 1, (size_t)len, f) != (size_t)len) ok = false; + fclose(f); + + if (!ok) { + remove(tmp); + safe_copy(err, err_len, "failed to write file"); + return false; + } + if (rename(tmp, path) != 0) { + remove(tmp); + safe_copy(err, err_len, "failed to rename file"); + return false; + } + return true; +} + +inline bool copy_file_atomic(const char* src, const char* dst, int* out_size, char* err, int err_len) { + uint8_t* data = nullptr; + int len = 0; + if (!read_file_bytes(src, &data, &len, err, err_len)) return false; + bool ok = write_file_atomic(dst, data, len, err, err_len); + free(data); + if (ok && out_size) *out_size = len; + return ok; +} + +inline bool read_default_printer_uri(char* out, int out_len) { + return read_text_file_line(DEFAULT_PRINTER_PATH, out, out_len); +} + +inline bool write_default_printer_uri(const char* uri) { + ensure_spool_dirs(); + return write_text_file_atomic(DEFAULT_PRINTER_PATH, uri); +} + +inline bool parse_args(ArgList* out) { + if (out == nullptr) return false; + memset(out, 0, sizeof(*out)); + int len = montauk::getargs(out->storage, sizeof(out->storage)); + if (len < 0) len = 0; + if (len >= (int)sizeof(out->storage)) len = (int)sizeof(out->storage) - 1; + out->storage[len] = '\0'; + + char* p = out->storage; + while (*p && out->count < (int)(sizeof(out->argv) / sizeof(out->argv[0]))) { + while (*p == ' ' || *p == '\t') p++; + if (*p == '\0') break; + + char quote = 0; + if (*p == '"' || *p == '\'') { + quote = *p; + p++; + } + + out->argv[out->count++] = p; + if (quote) { + while (*p && *p != quote) p++; + } else { + while (*p && *p != ' ' && *p != '\t') p++; + } + if (*p == '\0') break; + *p++ = '\0'; + } + return true; +} + +inline bool parse_u16(const char* s, uint16_t* out) { + if (s == nullptr || *s == '\0' || out == nullptr) return false; + unsigned long val = 0; + for (const char* p = s; *p; p++) { + if (*p < '0' || *p > '9') return false; + val = val * 10 + (unsigned long)(*p - '0'); + if (val > 65535UL) return false; + } + *out = (uint16_t)val; + return true; +} + +inline bool parse_ipv4_literal(const char* s, uint32_t* out) { + if (s == nullptr || out == nullptr) return false; + uint32_t parts[4] = {0, 0, 0, 0}; + int part = 0; + const char* p = s; + while (*p) { + if (part >= 4) return false; + if (*p < '0' || *p > '9') return false; + uint32_t value = 0; + while (*p >= '0' && *p <= '9') { + value = value * 10u + (uint32_t)(*p - '0'); + if (value > 255u) return false; + p++; + } + parts[part++] = value; + if (*p == '.') { + p++; + continue; + } + if (*p != '\0') return false; + } + if (part != 4) return false; + *out = (parts[0] & 0xFFu) + | ((parts[1] & 0xFFu) << 8) + | ((parts[2] & 0xFFu) << 16) + | ((parts[3] & 0xFFu) << 24); + return true; +} + +inline bool resolve_host(const char* host, uint32_t* out_ip) { + if (host == nullptr || out_ip == nullptr) return false; + if (parse_ipv4_literal(host, out_ip)) return true; + *out_ip = montauk::resolve(host); + return *out_ip != 0; +} + +inline bool starts_with_ci(const char* s, const char* prefix) { + if (s == nullptr || prefix == nullptr) return false; + while (*prefix) { + char a = *s++; + char b = *prefix++; + if (a >= 'A' && a <= 'Z') a = (char)(a - 'A' + 'a'); + if (b >= 'A' && b <= 'Z') b = (char)(b - 'A' + 'a'); + if (a != b) return false; + } + return true; +} + +inline bool str_ieq(const char* a, const char* b) { + if (a == nullptr || b == nullptr) return false; + while (*a && *b) { + char ca = *a++; + char cb = *b++; + if (ca >= 'A' && ca <= 'Z') ca = (char)(ca - 'A' + 'a'); + if (cb >= 'A' && cb <= 'Z') cb = (char)(cb - 'A' + 'a'); + if (ca != cb) return false; + } + return *a == '\0' && *b == '\0'; +} + +inline bool ends_with_ci(const char* s, const char* suffix) { + if (s == nullptr || suffix == nullptr) return false; + int slen = (int)strlen(s); + int tlen = (int)strlen(suffix); + if (tlen > slen) return false; + return str_ieq(s + slen - tlen, suffix); +} + +inline bool host_looks_like_mdns(const char* host) { + return host != nullptr && ends_with_ci(host, ".local"); +} + +inline bool parse_ipp_uri_impl(const char* uri, IppUri* out, char* err, int err_len, bool require_resolve) { + IppUri local = {}; + if (out == nullptr) out = &local; + memset(out, 0, sizeof(*out)); + if (uri == nullptr || *uri == '\0') { + safe_copy(err, err_len, "missing printer URI"); + return false; + } + + const char* sep = strstr(uri, "://"); + if (!sep) { + safe_copy(err, err_len, "printer URI must include a scheme"); + return false; + } + + char scheme[8] = {}; + size_t scheme_len = (size_t)(sep - uri); + if (scheme_len == 0 || scheme_len >= sizeof(scheme)) { + safe_copy(err, err_len, "invalid printer URI scheme"); + return false; + } + memcpy(scheme, uri, scheme_len); + + bool use_tls = false; + uint16_t port = 0; + if (str_ieq(scheme, "ipp")) { + use_tls = false; + port = 631; + } else if (str_ieq(scheme, "ipps")) { + use_tls = true; + port = 631; + } else if (str_ieq(scheme, "http")) { + use_tls = false; + port = 80; + } else if (str_ieq(scheme, "https")) { + use_tls = true; + port = 443; + } else { + safe_copy(err, err_len, "unsupported printer URI scheme"); + return false; + } + + const char* host = sep + 3; + if (*host == '\0') { + safe_copy(err, err_len, "printer URI is missing a host"); + return false; + } + + const char* host_end = host; + while (*host_end && *host_end != ':' && *host_end != '/') host_end++; + size_t host_len = (size_t)(host_end - host); + if (host_len == 0 || host_len >= sizeof(out->host)) { + safe_copy(err, err_len, "printer host is invalid"); + return false; + } + + memcpy(out->host, host, host_len); + out->host[host_len] = '\0'; + out->use_tls = use_tls; + + const char* path = host_end; + if (*host_end == ':') { + char port_buf[8] = {}; + const char* port_start = host_end + 1; + const char* port_end = port_start; + while (*port_end && *port_end != '/') port_end++; + size_t port_len = (size_t)(port_end - port_start); + if (port_len == 0 || port_len >= sizeof(port_buf)) { + safe_copy(err, err_len, "printer port is invalid"); + return false; + } + memcpy(port_buf, port_start, port_len); + if (!parse_u16(port_buf, &port)) { + safe_copy(err, err_len, "printer port is invalid"); + return false; + } + path = port_end; + } + + out->port = port; + if (*path == '\0') safe_copy(out->path, sizeof(out->path), "/ipp/print"); + else safe_copy(out->path, sizeof(out->path), path); + if (out->path[0] != '/') { + char tmp[sizeof(out->path)]; + snprintf(tmp, sizeof(tmp), "/%s", out->path); + safe_copy(out->path, sizeof(out->path), tmp); + } + + if (require_resolve && !resolve_host(out->host, &out->ip)) { + char msg[160]; + if (host_looks_like_mdns(out->host)) + snprintf(msg, sizeof(msg), + "could not resolve \"%s\" (.local/mDNS not supported yet)", + out->host); + else + snprintf(msg, sizeof(msg), "could not resolve printer host \"%s\"", out->host); + safe_copy(err, err_len, msg); + return false; + } + + snprintf(out->normalized, sizeof(out->normalized), "%s://%s:%u%s", + out->use_tls ? "ipps" : "ipp", out->host, (unsigned)out->port, out->path); + return true; +} + +inline bool parse_ipp_uri(const char* uri, IppUri* out, char* err, int err_len) { + return parse_ipp_uri_impl(uri, out, err, err_len, true); +} + +inline bool normalize_ipp_uri(const char* uri, IppUri* out, char* err, int err_len) { + return parse_ipp_uri_impl(uri, out, err, err_len, false); +} + +inline bool infer_doc_format(const char* path, char* out, int out_len) { + if (path == nullptr || out == nullptr || out_len <= 0) return false; + const char* base = basename_ptr(path); + const char* dot = strrchr(base, '.'); + if (!dot) { + safe_copy(out, out_len, "application/octet-stream"); + return true; + } + if (str_ieq(dot, ".pdf")) safe_copy(out, out_len, "application/pdf"); + else if (str_ieq(dot, ".jpg") || str_ieq(dot, ".jpeg")) safe_copy(out, out_len, "image/jpeg"); + else if (str_ieq(dot, ".urf")) safe_copy(out, out_len, "image/urf"); + else if (str_ieq(dot, ".pwg")) safe_copy(out, out_len, "image/pwg-raster"); + else if (str_ieq(dot, ".txt") || str_ieq(dot, ".log") || str_ieq(dot, ".md") || str_ieq(dot, ".csv")) + safe_copy(out, out_len, "text/plain"); + else if (str_ieq(dot, ".ps")) safe_copy(out, out_len, "application/postscript"); + else safe_copy(out, out_len, "application/octet-stream"); + return true; +} + +inline void generate_job_id(char* out, int out_len) { + uint64_t now = montauk::get_milliseconds(); + uint32_t rnd = 0; + if (montauk::getrandom(&rnd, sizeof(rnd)) < 0) { + rnd = (uint32_t)(now ^ (uint64_t)montauk::getpid()); + } + snprintf(out, (size_t)out_len, "%llu-%04x", + (unsigned long long)now, + (unsigned)(rnd & 0xFFFFu)); +} + +inline bool save_job_to_path_atomic(const char* path, const JobMeta* job) { + if (path == nullptr || job == nullptr) return false; + char tmp[MAX_PATH_LEN]; + snprintf(tmp, sizeof(tmp), "%s.tmp", path); + + FILE* f = fopen(tmp, "wb"); + if (!f) return false; + + JobMeta copy = *job; + sanitize_field(copy.job_name); + sanitize_field(copy.user_name); + sanitize_field(copy.printer_uri); + sanitize_field(copy.doc_path); + sanitize_field(copy.doc_format); + sanitize_field(copy.source_name); + sanitize_field(copy.created_at); + sanitize_field(copy.updated_at); + sanitize_field(copy.status_message); + sanitize_field(copy.debug_info); + + fprintf(f, "id=%s\n", copy.id); + fprintf(f, "state=%s\n", copy.state); + fprintf(f, "source_kind=%s\n", copy.source_kind); + fprintf(f, "job_name=%s\n", copy.job_name); + fprintf(f, "user_name=%s\n", copy.user_name); + fprintf(f, "printer_uri=%s\n", copy.printer_uri); + fprintf(f, "doc_path=%s\n", copy.doc_path); + fprintf(f, "doc_format=%s\n", copy.doc_format); + fprintf(f, "source_name=%s\n", copy.source_name); + fprintf(f, "created_at=%s\n", copy.created_at); + fprintf(f, "updated_at=%s\n", copy.updated_at); + fprintf(f, "status_message=%s\n", copy.status_message); + fprintf(f, "debug_info=%s\n", copy.debug_info); + fprintf(f, "remote_job_id=%d\n", copy.remote_job_id); + fprintf(f, "size_bytes=%d\n", copy.size_bytes); + fclose(f); + + if (rename(tmp, path) != 0) { + remove(tmp); + return false; + } + return true; +} + +inline bool load_job_from_path(const char* path, JobMeta* job) { + if (path == nullptr || job == nullptr) return false; + zero_job(job); + + FILE* f = fopen(path, "rb"); + if (!f) return false; + + char line[384]; + while (fgets(line, sizeof(line), f) != nullptr) { + trim_line(line); + char* eq = strchr(line, '='); + if (!eq) continue; + *eq++ = '\0'; + + if (strcmp(line, "id") == 0) safe_copy(job->id, sizeof(job->id), eq); + else if (strcmp(line, "state") == 0) safe_copy(job->state, sizeof(job->state), eq); + else if (strcmp(line, "source_kind") == 0) safe_copy(job->source_kind, sizeof(job->source_kind), eq); + else if (strcmp(line, "job_name") == 0) safe_copy(job->job_name, sizeof(job->job_name), eq); + else if (strcmp(line, "user_name") == 0) safe_copy(job->user_name, sizeof(job->user_name), eq); + else if (strcmp(line, "printer_uri") == 0) safe_copy(job->printer_uri, sizeof(job->printer_uri), eq); + else if (strcmp(line, "doc_path") == 0) safe_copy(job->doc_path, sizeof(job->doc_path), eq); + else if (strcmp(line, "doc_format") == 0) safe_copy(job->doc_format, sizeof(job->doc_format), eq); + else if (strcmp(line, "source_name") == 0) safe_copy(job->source_name, sizeof(job->source_name), eq); + else if (strcmp(line, "created_at") == 0) safe_copy(job->created_at, sizeof(job->created_at), eq); + else if (strcmp(line, "updated_at") == 0) safe_copy(job->updated_at, sizeof(job->updated_at), eq); + else if (strcmp(line, "status_message") == 0) safe_copy(job->status_message, sizeof(job->status_message), eq); + else if (strcmp(line, "debug_info") == 0) safe_copy(job->debug_info, sizeof(job->debug_info), eq); + else if (strcmp(line, "remote_job_id") == 0) job->remote_job_id = atoi(eq); + else if (strcmp(line, "size_bytes") == 0) job->size_bytes = atoi(eq); + } + fclose(f); + return job->id[0] != '\0'; +} + +inline bool find_job(const char* id, char* out_path, int out_path_len, char* out_state, int out_state_len) { + if (id == nullptr || *id == '\0') return false; + const struct { + const char* dir; + const char* state; + } dirs[] = { + {SPOOL_QUEUE_DIR, "queued"}, + {SPOOL_ACTIVE_DIR, "printing"}, + {SPOOL_DONE_DIR, "completed"}, + {SPOOL_FAILED_DIR, "failed"}, + }; + + char probe[MAX_PATH_LEN]; + for (size_t i = 0; i < sizeof(dirs) / sizeof(dirs[0]); i++) { + make_job_file_path(dirs[i].dir, id, probe, sizeof(probe)); + if (file_exists(probe)) { + safe_copy(out_path, out_path_len, probe); + safe_copy(out_state, out_state_len, dirs[i].state); + return true; + } + } + return false; +} + +inline bool daemon_is_running(int* out_pid) { + if (out_pid) *out_pid = -1; + char buf[32]; + if (!read_text_file_line(SPOOL_DAEMON_PID_PATH, buf, sizeof(buf))) return false; + int pid = atoi(buf); + if (pid <= 0) return false; + + int handle = montauk::proc_open(pid); + if (handle < 0) return false; + uint32_t sig = montauk::wait_handle(handle, Montauk::IPC_SIGNAL_EXITED, 0); + montauk::close(handle); + if (sig & Montauk::IPC_SIGNAL_EXITED) return false; + if (out_pid) *out_pid = pid; + return true; +} + +inline bool ensure_daemon_running(char* err, int err_len) { + ensure_spool_dirs(); + int pid = -1; + if (daemon_is_running(&pid)) return true; + + int spawned = montauk::spawn(PRINTD_BINARY); + if (spawned < 0) { + safe_copy(err, err_len, "failed to start print daemon"); + return false; + } + + for (int i = 0; i < 20; i++) { + montauk::sleep_ms(100); + if (daemon_is_running(&pid)) return true; + } + + safe_copy(err, err_len, "print daemon did not start"); + return false; +} + +inline int pdf_escape_line(const char* src, char* out, int out_len) { + int pos = 0; + for (int i = 0; src[i] != '\0' && pos < out_len - 2; i++) { + if (src[i] == '(' || src[i] == ')' || src[i] == '\\') out[pos++] = '\\'; + out[pos++] = src[i]; + } + out[pos] = '\0'; + return pos; +} + +inline bool generate_test_page_text(uint8_t** out_data, int* out_len) { + if (out_data) *out_data = nullptr; + if (out_len) *out_len = 0; + + char timestamp[32]; + now_string(timestamp, sizeof(timestamp)); + + char body[1024]; + int n = snprintf(body, sizeof(body), + "MontaukOS Test Page\r\n" + "====================\r\n" + "\r\n" + "This page was generated by the MontaukOS userspace print spooler.\r\n" + "Protocol: Internet Printing Protocol (IPP)\r\n" + "Generated: %s\r\n" + "PID: %d\r\n" + "\r\n" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ\r\n" + "abcdefghijklmnopqrstuvwxyz\r\n" + "0123456789 !@#$%%^&*()[]{}<>?/\\\\|+-=_~\r\n", + timestamp, montauk::getpid()); + if (n < 0) return false; + + uint8_t* buf = (uint8_t*)malloc((size_t)n); + if (!buf) return false; + memcpy(buf, body, (size_t)n); + + if (out_data) *out_data = buf; + if (out_len) *out_len = n; + return true; +} + +inline bool generate_test_page_pdf(uint8_t** out_data, int* out_len) { + if (out_data) *out_data = nullptr; + if (out_len) *out_len = 0; + + char timestamp[32]; + now_string(timestamp, sizeof(timestamp)); + + const char* lines[] = { + "MontaukOS Test Page", + "Printed through the MontaukOS userspace IPP spooler.", + "Protocol: Internet Printing Protocol (IPP)", + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "abcdefghijklmnopqrstuvwxyz", + "0123456789 !@#$%^&*()[]{}<>?/\\|+-=_~", + nullptr, + }; + + char escaped[8][192]; + for (int i = 0; i < 6; i++) pdf_escape_line(lines[i], escaped[i], sizeof(escaped[i])); + pdf_escape_line(timestamp, escaped[6], sizeof(escaped[6])); + + char content[2048]; + int content_len = snprintf(content, sizeof(content), + "BT\n" + "/F1 24 Tf\n" + "72 744 Td\n" + "(%s) Tj\n" + "0 -32 Td\n" + "/F1 12 Tf\n" + "(%s) Tj\n" + "0 -18 Td\n" + "(%s) Tj\n" + "0 -18 Td\n" + "(Generated: %s) Tj\n" + "0 -28 Td\n" + "(%s) Tj\n" + "0 -18 Td\n" + "(%s) Tj\n" + "0 -18 Td\n" + "(%s) Tj\n" + "ET\n", + escaped[0], escaped[1], escaped[2], escaped[6], + escaped[3], escaped[4], escaped[5]); + if (content_len < 0 || content_len >= (int)sizeof(content)) return false; + + int pdf_cap = 4096 + content_len; + char* pdf = (char*)malloc((size_t)pdf_cap); + if (!pdf) return false; + + int xref[6] = {0, 0, 0, 0, 0, 0}; + int pos = 0; + + auto appendf = [&](const char* fmt, auto... args) -> bool { + int wrote = snprintf(pdf + pos, (size_t)(pdf_cap - pos), fmt, args...); + if (wrote < 0 || pos + wrote >= pdf_cap) return false; + pos += wrote; + return true; + }; + + if (!appendf("%%PDF-1.4\n%%MontaukOS\n")) { free(pdf); return false; } + + xref[1] = pos; + if (!appendf("1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n")) { free(pdf); return false; } + xref[2] = pos; + if (!appendf("2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n")) { free(pdf); return false; } + xref[3] = pos; + if (!appendf("3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>\nendobj\n")) { free(pdf); return false; } + xref[4] = pos; + if (!appendf("4 0 obj\n<< /Length %d >>\nstream\n", content_len)) { free(pdf); return false; } + if (pos + content_len + 32 >= pdf_cap) { free(pdf); return false; } + memcpy(pdf + pos, content, (size_t)content_len); + pos += content_len; + if (!appendf("endstream\nendobj\n")) { free(pdf); return false; } + xref[5] = pos; + if (!appendf("5 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n")) { free(pdf); return false; } + + int xref_pos = pos; + if (!appendf("xref\n0 6\n")) { free(pdf); return false; } + if (!appendf("%010d 65535 f \n", 0)) { free(pdf); return false; } + for (int i = 1; i <= 5; i++) { + if (!appendf("%010d 00000 n \n", xref[i])) { free(pdf); return false; } + } + if (!appendf("trailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n%d\n%%%%EOF\n", xref_pos)) { free(pdf); return false; } + + *out_data = (uint8_t*)pdf; + *out_len = pos; + return true; +} + +inline bool bb_init(ByteBuilder* b, int initial_cap) { + if (b == nullptr) return false; + b->len = 0; + b->cap = initial_cap > 0 ? initial_cap : 512; + b->data = (uint8_t*)malloc((size_t)b->cap); + return b->data != nullptr; +} + +inline void bb_free(ByteBuilder* b) { + if (b && b->data) free(b->data); + if (b) { + b->data = nullptr; + b->len = 0; + b->cap = 0; + } +} + +inline bool bb_ensure(ByteBuilder* b, int extra) { + if (b == nullptr || extra < 0) return false; + if (b->len + extra <= b->cap) return true; + int next = b->cap; + while (next < b->len + extra) next *= 2; + uint8_t* data = (uint8_t*)realloc(b->data, (size_t)next); + if (!data) return false; + b->data = data; + b->cap = next; + return true; +} + +inline bool bb_put8(ByteBuilder* b, uint8_t v) { + if (!bb_ensure(b, 1)) return false; + b->data[b->len++] = v; + return true; +} + +inline bool bb_put16(ByteBuilder* b, uint16_t v) { + if (!bb_ensure(b, 2)) return false; + b->data[b->len++] = (uint8_t)((v >> 8) & 0xFFu); + b->data[b->len++] = (uint8_t)(v & 0xFFu); + return true; +} + +inline bool bb_put32(ByteBuilder* b, uint32_t v) { + if (!bb_ensure(b, 4)) return false; + b->data[b->len++] = (uint8_t)((v >> 24) & 0xFFu); + b->data[b->len++] = (uint8_t)((v >> 16) & 0xFFu); + b->data[b->len++] = (uint8_t)((v >> 8) & 0xFFu); + b->data[b->len++] = (uint8_t)(v & 0xFFu); + return true; +} + +inline bool bb_put_bytes(ByteBuilder* b, const void* data, int len) { + if (len < 0 || !bb_ensure(b, len)) return false; + if (len > 0) memcpy(b->data + b->len, data, (size_t)len); + b->len += len; + return true; +} + +inline bool bb_put_attr_str(ByteBuilder* b, uint8_t tag, const char* name, const char* value) { + uint16_t name_len = (uint16_t)strlen(name); + uint16_t value_len = (uint16_t)strlen(value); + return bb_put8(b, tag) + && bb_put16(b, name_len) + && bb_put_bytes(b, name, name_len) + && bb_put16(b, value_len) + && bb_put_bytes(b, value, value_len); +} + +inline bool bb_put_attr_int(ByteBuilder* b, uint8_t tag, const char* name, int value) { + uint16_t name_len = (uint16_t)strlen(name); + return bb_put8(b, tag) + && bb_put16(b, name_len) + && bb_put_bytes(b, name, name_len) + && bb_put16(b, 4) + && bb_put32(b, (uint32_t)value); +} + +inline uint16_t be16(const uint8_t* p) { + return (uint16_t)(((uint16_t)p[0] << 8) | (uint16_t)p[1]); +} + +inline uint32_t be32(const uint8_t* p) { + return ((uint32_t)p[0] << 24) + | ((uint32_t)p[1] << 16) + | ((uint32_t)p[2] << 8) + | (uint32_t)p[3]; +} + +inline uint32_t next_request_id() { + static uint32_t request_id = 1; + return request_id++; +} + +inline int send_all_plain(int fd, const uint8_t* data, int len) { + static constexpr int MAX_SEND_CHUNK = 32768; + uint64_t deadline = montauk::get_milliseconds() + 15000; + int off = 0; + while (off < len) { + int chunk = len - off; + if (chunk > MAX_SEND_CHUNK) chunk = MAX_SEND_CHUNK; + + int n = montauk::send(fd, data + off, (uint32_t)chunk); + if (n > 0) { + off += n; + deadline = montauk::get_milliseconds() + 15000; + continue; + } + if (n < 0) return -1; + + uint32_t sig = montauk::wait_handle(fd, + Montauk::IPC_SIGNAL_WRITABLE | Montauk::IPC_SIGNAL_PEER_CLOSED, + 1000); + if (sig & Montauk::IPC_SIGNAL_PEER_CLOSED) return -1; + if (montauk::get_milliseconds() >= deadline) return -1; + montauk::sleep_ms(1); + } + return off; +} + +inline bool response_has_no_body(int status) { + return (status >= 100 && status < 200) || status == 204 || status == 304; +} + +inline int parse_content_length_value(const http::Response* resp) { + char value[32] = {}; + if (!http::get_header(resp, "Content-Length", value, sizeof(value))) return -1; + char* end = nullptr; + long n = strtol(value, &end, 10); + if (end == value || n < 0) return -1; + return (int)n; +} + +inline bool chunked_body_complete(const char* src, int src_len) { + if (src == nullptr || src_len <= 0) return false; + + int pos = 0; + while (pos < src_len) { + int line_start = pos; + while (pos < src_len && src[pos] != '\n') pos++; + if (pos >= src_len) return false; + + int line_end = pos; + pos++; + while (line_end > line_start && (src[line_end - 1] == '\r' || src[line_end - 1] == '\n')) + line_end--; + + char hex[16] = {}; + int hex_pos = 0; + for (int i = line_start; i < line_end && hex_pos < (int)sizeof(hex) - 1; i++) { + if (src[i] == ';') break; + hex[hex_pos++] = src[i]; + } + if (hex_pos == 0) return false; + + char* end = nullptr; + unsigned long chunk = strtoul(hex, &end, 16); + if (end == hex) return false; + + if (chunk == 0) { + if (pos >= src_len) return false; + if (src[pos] == '\n') return true; + if (src[pos] == '\r' && pos + 1 < src_len && src[pos + 1] == '\n') return true; + return http::find_header_block_end(src + pos, src + src_len) != nullptr; + } + + if (pos + (int)chunk > src_len) return false; + pos += (int)chunk; + if (pos < src_len && src[pos] == '\r') pos++; + if (pos < src_len && src[pos] == '\n') pos++; + } + + return false; +} + +inline bool response_is_chunked(const http::Response* resp); + +inline bool http_response_complete(char* buf, int len, bool peer_closed) { + if (buf == nullptr || len <= 0) return false; + + http::Response resp = {}; + if (http::parse_response(buf, len, &resp) < 0) return false; + if (resp.body == nullptr) return false; + if (response_has_no_body(resp.status)) return true; + if (response_is_chunked(&resp)) return chunked_body_complete(resp.body, resp.body_len); + + int content_length = parse_content_length_value(&resp); + if (content_length >= 0) return resp.body_len >= content_length; + return peer_closed; +} + +inline int recv_http_plain(int fd, char* buf, int cap) { + int total = 0; + bool peer_closed = false; + uint64_t deadline = montauk::get_milliseconds() + 90000; + + while (total < cap - 1) { + if (http_response_complete(buf, total, peer_closed)) break; + if (montauk::get_milliseconds() >= deadline) break; + + uint32_t sig = montauk::wait_handle(fd, + Montauk::IPC_SIGNAL_READABLE | Montauk::IPC_SIGNAL_PEER_CLOSED, + 1000); + if (sig == 0) continue; + + if (sig & Montauk::IPC_SIGNAL_PEER_CLOSED) + peer_closed = true; + if (!(sig & Montauk::IPC_SIGNAL_READABLE)) { + if (peer_closed) break; + continue; + } + + int n = montauk::recv(fd, buf + total, (uint32_t)(cap - 1 - total)); + if (n < 0) break; + if (n == 0) { + if (peer_closed) break; + continue; + } + total += n; + deadline = montauk::get_milliseconds() + 90000; + } + buf[total] = '\0'; + return total; +} + +inline bool response_is_chunked(const http::Response* resp) { + char value[64] = {}; + if (!http::get_header(resp, "Transfer-Encoding", value, sizeof(value))) return false; + for (int i = 0; value[i]; i++) { + if (value[i] >= 'A' && value[i] <= 'Z') value[i] = (char)(value[i] - 'A' + 'a'); + } + return strstr(value, "chunked") != nullptr; +} + +inline bool extract_http_body(const http::Response* resp, uint8_t** out_body, int* out_len) { + if (out_body) *out_body = nullptr; + if (out_len) *out_len = 0; + if (resp == nullptr || resp->body == nullptr || resp->body_len < 0) return false; + + if (!response_is_chunked(resp)) { + uint8_t* body = (uint8_t*)malloc((size_t)(resp->body_len > 0 ? resp->body_len : 1)); + if (!body) return false; + if (resp->body_len > 0) memcpy(body, resp->body, (size_t)resp->body_len); + if (out_body) *out_body = body; + if (out_len) *out_len = resp->body_len; + return true; + } + + const char* src = resp->body; + int src_len = resp->body_len; + int pos = 0; + int out_pos = 0; + uint8_t* body = (uint8_t*)malloc((size_t)src_len); + if (!body) return false; + + while (pos < src_len) { + int line_start = pos; + while (pos < src_len && src[pos] != '\n') pos++; + int line_end = pos; + if (pos < src_len && src[pos] == '\n') pos++; + while (line_end > line_start && (src[line_end - 1] == '\r' || src[line_end - 1] == '\n')) + line_end--; + + char hex[16] = {}; + int hex_pos = 0; + for (int i = line_start; i < line_end && hex_pos < (int)sizeof(hex) - 1; i++) { + if (src[i] == ';') break; + hex[hex_pos++] = src[i]; + } + unsigned long chunk = strtoul(hex, nullptr, 16); + if (chunk == 0) { + if (out_body) *out_body = body; + if (out_len) *out_len = out_pos; + return true; + } + if (pos + (int)chunk > src_len) break; + memcpy(body + out_pos, src + pos, chunk); + out_pos += (int)chunk; + pos += (int)chunk; + if (pos < src_len && src[pos] == '\r') pos++; + if (pos < src_len && src[pos] == '\n') pos++; + } + + free(body); + return false; +} + +inline bool ipp_http_post(const IppUri* uri, + const uint8_t* body, int body_len, + uint8_t** out_body, int* out_body_len, + int* out_http_status, + char* err, int err_len) { + if (out_body) *out_body = nullptr; + if (out_body_len) *out_body_len = 0; + if (out_http_status) *out_http_status = -1; + if (uri == nullptr || body == nullptr || body_len < 0) { + safe_copy(err, err_len, "invalid IPP request"); + return false; + } + + char ip_text[20]; + format_ipv4(ip_text, sizeof(ip_text), uri->ip); + + char host_header[160]; + if (uri->port == 631 || uri->port == 80 || uri->port == 443) + safe_copy(host_header, sizeof(host_header), uri->host); + else + snprintf(host_header, sizeof(host_header), "%s:%u", uri->host, (unsigned)uri->port); + + int req_cap = body_len + 1024; + char* req = (char*)malloc((size_t)req_cap); + if (!req) { + safe_copy(err, err_len, "out of memory"); + return false; + } + + int req_len = snprintf(req, (size_t)req_cap, + "POST %s HTTP/1.1\r\n" + "Host: %s\r\n" + "User-Agent: MontaukOS Print/1.0\r\n" + "Content-Type: application/ipp\r\n" + "Content-Length: %d\r\n" + "Connection: close\r\n" + "\r\n", + uri->path, host_header, body_len); + if (req_len < 0 || req_len + body_len >= req_cap) { + free(req); + safe_copy(err, err_len, "IPP request is too large"); + return false; + } + memcpy(req + req_len, body, (size_t)body_len); + req_len += body_len; + + char* raw = (char*)malloc(HTTP_RESPONSE_MAX); + if (!raw) { + free(req); + safe_copy(err, err_len, "out of memory"); + return false; + } + + int raw_len = -1; + if (uri->use_tls) { + tls::TrustAnchors tas = tls::load_trust_anchors(); + raw_len = tls::https_fetch(uri->host, uri->ip, uri->port, req, req_len, + tas, raw, HTTP_RESPONSE_MAX - 1); + if (tas.anchors) free(tas.anchors); + } else { + int fd = montauk::socket(Montauk::SOCK_TCP); + if (fd < 0) { + free(raw); + free(req); + snprintf(err, (size_t)err_len, "failed to create socket for %s:%u", uri->host, (unsigned)uri->port); + return false; + } + if (montauk::connect(fd, uri->ip, uri->port) < 0) { + montauk::closesocket(fd); + free(raw); + free(req); + snprintf(err, (size_t)err_len, "failed to connect to %s (%s):%u", + uri->host, ip_text, (unsigned)uri->port); + return false; + } + if (send_all_plain(fd, (const uint8_t*)req, req_len) < 0) { + montauk::closesocket(fd); + free(raw); + free(req); + snprintf(err, (size_t)err_len, "failed to send print request to %s (%s):%u", + uri->host, ip_text, (unsigned)uri->port); + return false; + } + raw_len = recv_http_plain(fd, raw, HTTP_RESPONSE_MAX); + montauk::closesocket(fd); + } + + free(req); + + if (raw_len <= 0) { + free(raw); + snprintf(err, (size_t)err_len, "printer returned no response from %s (%s):%u", + uri->host, ip_text, (unsigned)uri->port); + return false; + } + + raw[raw_len] = '\0'; + http::Response resp = {}; + if (http::parse_response(raw, raw_len, &resp) < 0) { + free(raw); + snprintf(err, (size_t)err_len, "printer returned no final HTTP response from %s (%s):%u", + uri->host, ip_text, (unsigned)uri->port); + return false; + } + if (out_http_status) *out_http_status = resp.status; + if (resp.status < 200 || resp.status >= 300) { + free(raw); + snprintf(err, (size_t)err_len, "HTTP %d from %s (%s):%u%s", + resp.status, uri->host, ip_text, (unsigned)uri->port, uri->path); + return false; + } + + bool ok = extract_http_body(&resp, out_body, out_body_len); + free(raw); + if (!ok) { + snprintf(err, (size_t)err_len, "failed to parse printer response from %s (%s):%u", + uri->host, ip_text, (unsigned)uri->port); + return false; + } + return true; +} + +inline void summarize_ipp_capabilities(const IppCapabilities* caps, char* out, int out_len) { + if (out == nullptr || out_len <= 0) return; + out[0] = '\0'; + if (caps == nullptr) return; + char ip_text[20]; + if (caps->resolved_ip) format_ipv4(ip_text, sizeof(ip_text), caps->resolved_ip); + else safe_copy(ip_text, sizeof(ip_text), "unresolved"); + snprintf(out, (size_t)out_len, + "fmts=%s%s%s host=%s ip=%s port=%u path=%s %s http=%d ipp=0x%04x req=%d pdf=%s jpeg=%s text=%s", + caps->supported_formats[0] ? caps->supported_formats : "?", + caps->preferred_format[0] ? " pref=" : "", + caps->preferred_format[0] ? caps->preferred_format : "", + caps->host[0] ? caps->host : "?", + ip_text, + (unsigned)caps->port, + caps->path[0] ? caps->path : "/", + caps->use_tls ? "tls" : "plain", + caps->http_status, + (unsigned)caps->ipp_status, + caps->request_id, + caps->supports_pdf ? "yes" : "no", + caps->supports_jpeg ? "yes" : "no", + caps->supports_text ? "yes" : "no"); +} + +inline void summarize_ipp_print_result(const IppPrintResult* result, char* out, int out_len) { + if (out == nullptr || out_len <= 0) return; + out[0] = '\0'; + if (result == nullptr) return; + char ip_text[20]; + if (result->resolved_ip) format_ipv4(ip_text, sizeof(ip_text), result->resolved_ip); + else safe_copy(ip_text, sizeof(ip_text), "unresolved"); + snprintf(out, (size_t)out_len, + "host=%s ip=%s port=%u path=%s %s format=%s http=%d ipp=0x%04x req=%d job=%d", + result->host[0] ? result->host : "?", + ip_text, + (unsigned)result->port, + result->path[0] ? result->path : "/", + result->use_tls ? "tls" : "plain", + result->document_format[0] ? result->document_format : "?", + result->http_status, + (unsigned)result->ipp_status, + result->request_id, + result->job_id); +} + +inline void ipp_copy_value(char* out, int out_len, const uint8_t* data, int len) { + if (out == nullptr || out_len <= 0) return; + int n = len; + if (n >= out_len) n = out_len - 1; + memcpy(out, data, (size_t)n); + out[n] = '\0'; +} + +inline void ipp_append_list_value(char* out, int out_len, const char* value) { + if (out == nullptr || out_len <= 0 || value == nullptr || value[0] == '\0') return; + if (out[0] != '\0') { + size_t len = strlen(out); + if ((int)len < out_len - 1) { + out[len++] = ','; + out[len] = '\0'; + } + } + safe_copy(out + strlen(out), out_len - (int)strlen(out), value); +} + +inline bool ipp_parse_common(const uint8_t* body, int body_len, + uint16_t* out_status, int* out_request_id, + IppCapabilities* caps, IppPrintResult* result, + char* err, int err_len) { + if (body == nullptr || body_len < 8) { + safe_copy(err, err_len, "IPP response is malformed"); + return false; + } + + if (out_status) *out_status = be16(body + 2); + if (out_request_id) *out_request_id = (int)be32(body + 4); + + char current_name[128] = {}; + int pos = 8; + while (pos < body_len) { + uint8_t tag = body[pos++]; + if (tag == 0x03) break; + if (tag <= 0x0F) { + current_name[0] = '\0'; + continue; + } + if (pos + 4 > body_len) break; + uint16_t name_len = be16(body + pos); + pos += 2; + if (pos + name_len + 2 > body_len) break; + if (name_len > 0) { + int copy_len = (int)name_len; + if (copy_len >= (int)sizeof(current_name)) copy_len = (int)sizeof(current_name) - 1; + memcpy(current_name, body + pos, (size_t)copy_len); + current_name[copy_len] = '\0'; + } + pos += name_len; + uint16_t value_len = be16(body + pos); + pos += 2; + if (pos + value_len > body_len) break; + const uint8_t* value = body + pos; + pos += value_len; + + if (caps) { + if (strcmp(current_name, "printer-name") == 0) { + ipp_copy_value(caps->printer_name, sizeof(caps->printer_name), value, value_len); + } else if (strcmp(current_name, "status-message") == 0) { + ipp_copy_value(caps->status_message, sizeof(caps->status_message), value, value_len); + } else if (strcmp(current_name, "document-format-supported") == 0) { + char fmt[96]; + ipp_copy_value(fmt, sizeof(fmt), value, value_len); + ipp_append_list_value(caps->supported_formats, sizeof(caps->supported_formats), fmt); + if (str_ieq(fmt, "application/pdf")) caps->supports_pdf = true; + if (str_ieq(fmt, "image/jpeg")) caps->supports_jpeg = true; + if (starts_with_ci(fmt, "text/plain")) caps->supports_text = true; + } else if (strcmp(current_name, "document-format-default") == 0) { + ipp_copy_value(caps->default_format, sizeof(caps->default_format), value, value_len); + } else if (strcmp(current_name, "document-format-preferred") == 0) { + ipp_copy_value(caps->preferred_format, sizeof(caps->preferred_format), value, value_len); + } + } + + if (result) { + if (strcmp(current_name, "job-id") == 0 && value_len == 4) { + result->job_id = (int)be32(value); + } else if (strcmp(current_name, "job-uri") == 0) { + ipp_copy_value(result->job_uri, sizeof(result->job_uri), value, value_len); + } else if (strcmp(current_name, "status-message") == 0) { + ipp_copy_value(result->status_message, sizeof(result->status_message), value, value_len); + } + } + } + return true; +} + +inline bool ipp_get_printer_capabilities(const char* printer_uri, + IppCapabilities* out, + char* err, int err_len) { + if (out) memset(out, 0, sizeof(*out)); + IppUri uri = {}; + if (!parse_ipp_uri(printer_uri, &uri, err, err_len)) return false; + if (out) { + out->use_tls = uri.use_tls; + out->port = uri.port; + out->resolved_ip = uri.ip; + safe_copy(out->host, sizeof(out->host), uri.host); + safe_copy(out->path, sizeof(out->path), uri.path); + } + + ByteBuilder req = {}; + if (!bb_init(&req, 512)) { + safe_copy(err, err_len, "out of memory"); + return false; + } + + uint32_t request_id = next_request_id(); + if (out) out->request_id = (int)request_id; + bool ok = bb_put8(&req, 0x01) && bb_put8(&req, 0x01) + && bb_put16(&req, 0x000B) + && bb_put32(&req, request_id) + && bb_put8(&req, 0x01) + && bb_put_attr_str(&req, 0x47, "attributes-charset", "utf-8") + && bb_put_attr_str(&req, 0x48, "attributes-natural-language", "en") + && bb_put_attr_str(&req, 0x45, "printer-uri", uri.normalized) + && bb_put_attr_str(&req, 0x44, "requested-attributes", "printer-name") + && bb_put8(&req, 0x44) + && bb_put16(&req, 0) + && bb_put16(&req, (uint16_t)strlen("printer-state")) + && bb_put_bytes(&req, "printer-state", (int)strlen("printer-state")) + && bb_put8(&req, 0x44) + && bb_put16(&req, 0) + && bb_put16(&req, (uint16_t)strlen("document-format-supported")) + && bb_put_bytes(&req, "document-format-supported", (int)strlen("document-format-supported")) + && bb_put8(&req, 0x44) + && bb_put16(&req, 0) + && bb_put16(&req, (uint16_t)strlen("document-format-default")) + && bb_put_bytes(&req, "document-format-default", (int)strlen("document-format-default")) + && bb_put8(&req, 0x44) + && bb_put16(&req, 0) + && bb_put16(&req, (uint16_t)strlen("document-format-preferred")) + && bb_put_bytes(&req, "document-format-preferred", (int)strlen("document-format-preferred")) + && bb_put8(&req, 0x03); + if (!ok) { + bb_free(&req); + safe_copy(err, err_len, "failed to build IPP capability request"); + return false; + } + + uint8_t* body = nullptr; + int body_len = 0; + int http_status = -1; + ok = ipp_http_post(&uri, req.data, req.len, &body, &body_len, &http_status, err, err_len); + bb_free(&req); + if (!ok) { + if (out) out->http_status = http_status; + return false; + } + + uint16_t ipp_status = 0; + int response_id = 0; + if (!ipp_parse_common(body, body_len, &ipp_status, &response_id, out, nullptr, err, err_len)) { + free(body); + return false; + } + free(body); + + if (out) { + out->ok = (ipp_status == 0x0000); + out->http_status = http_status; + out->ipp_status = ipp_status; + out->request_id = response_id; + } + if (ipp_status != 0x0000) { + safe_copy(err, err_len, out ? out->status_message : "IPP capability query failed"); + return false; + } + return true; +} + +inline bool ipp_print_buffer(const char* printer_uri, + const char* job_name, + const char* user_name, + const char* doc_format, + const uint8_t* data, int data_len, + IppPrintResult* out, + char* err, int err_len) { + if (out) memset(out, 0, sizeof(*out)); + IppUri uri = {}; + if (!parse_ipp_uri(printer_uri, &uri, err, err_len)) return false; + if (out) { + out->use_tls = uri.use_tls; + out->port = uri.port; + out->resolved_ip = uri.ip; + safe_copy(out->host, sizeof(out->host), uri.host); + safe_copy(out->path, sizeof(out->path), uri.path); + } + if (data == nullptr || data_len < 0) { + safe_copy(err, err_len, "missing print document"); + return false; + } + + ByteBuilder req = {}; + if (!bb_init(&req, 1024 + data_len)) { + safe_copy(err, err_len, "out of memory"); + return false; + } + + uint32_t request_id = next_request_id(); + const char* use_job_name = (job_name && *job_name) ? job_name : "MontaukOS Print Job"; + const char* use_user = (user_name && *user_name) ? user_name : "montauk"; + const char* use_format = (doc_format && *doc_format) ? doc_format : "application/octet-stream"; + if (out) { + out->request_id = (int)request_id; + safe_copy(out->document_format, sizeof(out->document_format), use_format); + } + + bool ok = bb_put8(&req, 0x01) && bb_put8(&req, 0x01) + && bb_put16(&req, 0x0002) + && bb_put32(&req, request_id) + && bb_put8(&req, 0x01) + && bb_put_attr_str(&req, 0x47, "attributes-charset", "utf-8") + && bb_put_attr_str(&req, 0x48, "attributes-natural-language", "en") + && bb_put_attr_str(&req, 0x45, "printer-uri", uri.normalized) + && bb_put_attr_str(&req, 0x42, "requesting-user-name", use_user) + && bb_put_attr_str(&req, 0x42, "job-name", use_job_name) + && bb_put_attr_str(&req, 0x49, "document-format", use_format) + && bb_put8(&req, 0x03) + && bb_put_bytes(&req, data, data_len); + if (!ok) { + bb_free(&req); + safe_copy(err, err_len, "failed to build IPP print request"); + return false; + } + + uint8_t* body = nullptr; + int body_len = 0; + int http_status = -1; + ok = ipp_http_post(&uri, req.data, req.len, &body, &body_len, &http_status, err, err_len); + bb_free(&req); + if (!ok) { + if (out) out->http_status = http_status; + return false; + } + + uint16_t ipp_status = 0; + int response_id = 0; + if (!ipp_parse_common(body, body_len, &ipp_status, &response_id, nullptr, out, err, err_len)) { + free(body); + return false; + } + free(body); + + if (out) { + out->http_status = http_status; + out->ipp_status = ipp_status; + out->request_id = response_id; + out->ok = (ipp_status == 0x0000 || ipp_status == 0x0001); + if (out->status_message[0] == '\0') { + snprintf(out->status_message, sizeof(out->status_message), + "IPP status 0x%04x", (unsigned)ipp_status); + } + } + + if (ipp_status != 0x0000 && ipp_status != 0x0001) { + if (out && out->status_message[0]) safe_copy(err, err_len, out->status_message); + else safe_copy(err, err_len, "print job failed"); + return false; + } + return true; +} + +inline bool submit_document_job(const char* source_path, + const char* printer_uri, + const char* job_name_override, + char* out_job_id, int out_job_id_len, + char* err, int err_len) { + ensure_spool_dirs(); + + char resolved_uri[MAX_PATH_LEN]; + if (printer_uri && *printer_uri) safe_copy(resolved_uri, sizeof(resolved_uri), printer_uri); + else if (!read_default_printer_uri(resolved_uri, sizeof(resolved_uri))) { + safe_copy(err, err_len, "no default printer is configured"); + return false; + } + + IppUri uri = {}; + if (!parse_ipp_uri(resolved_uri, &uri, err, err_len)) return false; + + char job_id[48]; + generate_job_id(job_id, sizeof(job_id)); + + char doc_path[MAX_PATH_LEN]; + make_doc_file_path(job_id, doc_path, sizeof(doc_path)); + + int size_bytes = 0; + if (!copy_file_atomic(source_path, doc_path, &size_bytes, err, err_len)) return false; + + JobMeta job = {}; + safe_copy(job.id, sizeof(job.id), job_id); + safe_copy(job.state, sizeof(job.state), "queued"); + safe_copy(job.source_kind, sizeof(job.source_kind), "document"); + safe_copy(job.job_name, sizeof(job.job_name), + (job_name_override && *job_name_override) ? job_name_override : basename_ptr(source_path)); + safe_copy(job.user_name, sizeof(job.user_name), "montauk"); + safe_copy(job.printer_uri, sizeof(job.printer_uri), uri.normalized); + safe_copy(job.doc_path, sizeof(job.doc_path), doc_path); + infer_doc_format(source_path, job.doc_format, sizeof(job.doc_format)); + safe_copy(job.source_name, sizeof(job.source_name), basename_ptr(source_path)); + now_string(job.created_at, sizeof(job.created_at)); + safe_copy(job.updated_at, sizeof(job.updated_at), job.created_at); + safe_copy(job.status_message, sizeof(job.status_message), "Queued"); + snprintf(job.debug_info, sizeof(job.debug_info), + "printer=%s format=%s source=%s", + uri.normalized, job.doc_format, job.source_name); + job.remote_job_id = 0; + job.size_bytes = size_bytes; + + char job_path[MAX_PATH_LEN]; + make_job_file_path(SPOOL_QUEUE_DIR, job.id, job_path, sizeof(job_path)); + if (!save_job_to_path_atomic(job_path, &job)) { + remove(doc_path); + safe_copy(err, err_len, "failed to create queued job"); + return false; + } + + if (!ensure_daemon_running(err, err_len)) return false; + + safe_copy(out_job_id, out_job_id_len, job.id); + return true; +} + +inline bool submit_test_page_job(const char* printer_uri, + char* out_job_id, int out_job_id_len, + char* err, int err_len) { + ensure_spool_dirs(); + + char resolved_uri[MAX_PATH_LEN]; + if (printer_uri && *printer_uri) safe_copy(resolved_uri, sizeof(resolved_uri), printer_uri); + else if (!read_default_printer_uri(resolved_uri, sizeof(resolved_uri))) { + safe_copy(err, err_len, "no default printer is configured"); + return false; + } + + IppUri uri = {}; + if (!parse_ipp_uri(resolved_uri, &uri, err, err_len)) return false; + + JobMeta job = {}; + generate_job_id(job.id, sizeof(job.id)); + safe_copy(job.state, sizeof(job.state), "queued"); + safe_copy(job.source_kind, sizeof(job.source_kind), "test-page"); + safe_copy(job.job_name, sizeof(job.job_name), "MontaukOS Test Page"); + safe_copy(job.user_name, sizeof(job.user_name), "montauk"); + safe_copy(job.printer_uri, sizeof(job.printer_uri), uri.normalized); + safe_copy(job.doc_path, sizeof(job.doc_path), ""); + safe_copy(job.doc_format, sizeof(job.doc_format), "auto"); + safe_copy(job.source_name, sizeof(job.source_name), "test-page"); + now_string(job.created_at, sizeof(job.created_at)); + safe_copy(job.updated_at, sizeof(job.updated_at), job.created_at); + safe_copy(job.status_message, sizeof(job.status_message), "Queued"); + snprintf(job.debug_info, sizeof(job.debug_info), + "printer=%s format=auto source=%s", + uri.normalized, job.source_name); + + char job_path[MAX_PATH_LEN]; + make_job_file_path(SPOOL_QUEUE_DIR, job.id, job_path, sizeof(job_path)); + if (!save_job_to_path_atomic(job_path, &job)) { + safe_copy(err, err_len, "failed to create test page job"); + return false; + } + + if (!ensure_daemon_running(err, err_len)) return false; + + safe_copy(out_job_id, out_job_id_len, job.id); + return true; +} + +inline bool wait_for_job(const char* job_id, JobMeta* out_job, uint64_t timeout_ms, + char* err, int err_len) { + uint64_t start = montauk::get_milliseconds(); + char path[MAX_PATH_LEN]; + char state[16]; + while (true) { + if (find_job(job_id, path, sizeof(path), state, sizeof(state))) { + JobMeta job = {}; + if (load_job_from_path(path, &job)) { + if (strcmp(state, "completed") == 0 || strcmp(state, "failed") == 0) { + if (out_job) *out_job = job; + return strcmp(state, "completed") == 0; + } + } + } + if (montauk::get_milliseconds() - start >= timeout_ms) { + safe_copy(err, err_len, "timed out waiting for print job"); + return false; + } + montauk::sleep_ms(JOB_WAIT_SLICE_MS); + } +} + +} // namespace print diff --git a/programs/man/printctl.1 b/programs/man/printctl.1 new file mode 100644 index 0000000..3e58e44 --- /dev/null +++ b/programs/man/printctl.1 @@ -0,0 +1,32 @@ +.TH PRINTCTL 1 +.SH NAME +printctl \- configure printers and submit print jobs +.SH SYNOPSIS +.B printctl +.I command +.RI [ options ] +.SH DESCRIPTION +.B printctl +manages the MontaukOS userspace print spooler and submits print jobs to IPP printers. +.SH COMMANDS +.TP +.B set-printer \fIURI\fR +Store the default printer URI. +.TP +.B show-printer +Print the configured default printer URI. +.TP +.B print \fIFILE\fR [\-\-printer \fIURI\fR] [\-\-name \fIJOB\fR] [\-\-wait] +Queue a file for printing. +.TP +.B test-page [\-\-printer \fIURI\fR] [\-\-wait] [\-\-no-wait] +Generate and queue a simple test page. +.TP +.B status [\-\-verbose] +Show daemon state and queued, active, completed, and failed jobs. +.TP +.B inspect \fIJOB-ID\fR +Show full metadata and debug details for a queued, active, completed, or failed job. +.TP +.B probe [\fIURI\fR] +Probe the configured printer, print host and resolution details, and show IPP capability diagnostics. diff --git a/programs/man/printd.1 b/programs/man/printd.1 new file mode 100644 index 0000000..414a94c --- /dev/null +++ b/programs/man/printd.1 @@ -0,0 +1,12 @@ +.TH PRINTD 1 +.SH NAME +printd \- MontaukOS userspace print spooler daemon +.SH SYNOPSIS +.B printd +.SH DESCRIPTION +.B printd +monitors the print spool directories, claims queued jobs, and delivers them to IPP printers. + +It is normally launched automatically by +.BR init (1) +and does not require direct user interaction. diff --git a/programs/src/desktop/apps/app_settings.cpp b/programs/src/desktop/apps/app_settings.cpp index d5ed7b1..224ad36 100644 --- a/programs/src/desktop/apps/app_settings.cpp +++ b/programs/src/desktop/apps/app_settings.cpp @@ -91,6 +91,16 @@ static bool color_eq(Color a, Color b) { return a.r == b.r && a.g == b.g && a.b == b.b; } +static bool settings_status_visible(SettingsState* st) { + return st->status_msg[0] && + (montauk::get_milliseconds() - st->status_time < 4000); +} + +static void settings_set_status(SettingsState* st, const char* msg) { + montauk::strncpy(st->status_msg, msg ? msg : "", (int)sizeof(st->status_msg)); + st->status_time = montauk::get_milliseconds(); +} + // ============================================================================ // Helper: find selected swatch index in a palette // ============================================================================ @@ -706,7 +716,6 @@ static void adduser_on_mouse(Window* win, MouseEvent& ev) { int my = ev.y - cr.y; int pad = 16; int sfh = system_font_height(); - int fw = win->content_w - 2 * pad; int y = pad; // Username field hit diff --git a/programs/src/init/main.cpp b/programs/src/init/main.cpp index be08797..06da618 100644 --- a/programs/src/init/main.cpp +++ b/programs/src/init/main.cpp @@ -156,6 +156,7 @@ extern "C" void _start() { // ---- Stage 1: Network configuration (non-blocking) ---- run_service("0:/os/dhcp.elf", "dhcp", false); + run_service("0:/os/printd.elf", "print spooler", false); // ---- Stage 2: Login screen -> desktop (falls back to desktop, then shell) ---- if (!run_service("0:/os/login.elf", "login")) { diff --git a/programs/src/printctl/Makefile b/programs/src/printctl/Makefile new file mode 100644 index 0000000..acaa75d --- /dev/null +++ b/programs/src/printctl/Makefile @@ -0,0 +1,77 @@ +# Makefile for printctl on MontaukOS + +MAKEFLAGS += -rR +.SUFFIXES: + +TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf- +ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),) + CXX := $(TOOLCHAIN_PREFIX)g++ +else + CXX := g++ +endif + +PROG_INC := ../../include +LIBC_LIB := ../../lib/libc +BEARSSL := ../../lib/bearssl +TLS_LIB := ../../lib/tls +LINK_LD := ../../link.ld +BINDIR := ../../bin +OBJDIR := obj + +CXXFLAGS := \ + -std=gnu++20 \ + -g -O2 -pipe \ + -Wall \ + -Wextra \ + -Wno-unused-parameter \ + -nostdinc \ + -ffreestanding \ + -fno-stack-protector \ + -fno-stack-check \ + -fno-PIC \ + -fno-rtti \ + -fno-exceptions \ + -ffunction-sections \ + -fdata-sections \ + -m64 \ + -march=x86-64 \ + -mno-80387 \ + -mno-mmx \ + -mno-sse \ + -mno-sse2 \ + -mno-red-zone \ + -mcmodel=small \ + -I $(PROG_INC) \ + -isystem $(PROG_INC)/libc \ + -I $(BEARSSL)/inc \ + -isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \ + -isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include + +LDFLAGS := \ + -nostdlib \ + -static \ + -Wl,--build-id=none \ + -Wl,--gc-sections \ + -Wl,-m,elf_x86_64 \ + -z max-page-size=0x1000 \ + -T $(LINK_LD) + +TARGET := $(BINDIR)/os/printctl.elf +DEPS := $(OBJDIR)/main.d + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): $(OBJDIR)/main.o $(LINK_LD) Makefile + mkdir -p $(BINDIR)/os + $(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJDIR)/main.o $(TLS_LIB)/libtls.a $(BEARSSL)/libbearssl.a $(LIBC_LIB)/liblibc.a -o $@ + +$(OBJDIR)/main.o: main.cpp Makefile + mkdir -p $(OBJDIR) + $(CXX) $(CXXFLAGS) -MMD -MP -c $< -o $@ + +-include $(DEPS) + +clean: + rm -rf $(OBJDIR) $(TARGET) diff --git a/programs/src/printctl/main.cpp b/programs/src/printctl/main.cpp new file mode 100644 index 0000000..629f8ed --- /dev/null +++ b/programs/src/printctl/main.cpp @@ -0,0 +1,360 @@ +/* + * main.cpp + * MontaukOS print control utility + * Copyright (c) 2026 Daniel Hammer + */ + +#include + +extern "C" { +#include +#include +#include +} + +using namespace print; + +static void usage() { + printf("Usage:\n"); + printf(" printctl set-printer \n"); + printf(" printctl show-printer\n"); + printf(" printctl print [--printer ] [--name ] [--wait]\n"); + printf(" printctl test-page [--printer ] [--wait] [--no-wait]\n"); + printf(" printctl status [--verbose]\n"); + printf(" printctl inspect \n"); + printf(" printctl probe [uri]\n"); +} + +static void print_host_details(const IppUri& uri) { + char ip[20]; + if (uri.ip) format_ipv4(ip, sizeof(ip), uri.ip); + else safe_copy(ip, sizeof(ip), "unresolved"); + + printf("Host: %s\n", uri.host); + printf("Resolved IP: %s\n", ip); + printf("Transport: %s\n", uri.use_tls ? "TLS" : "Plain TCP"); + printf("Port: %u\n", (unsigned)uri.port); + printf("Path: %s\n", uri.path); +} + +static void print_job_detail(const JobMeta& job, const char* state, bool verbose) { + printf(" %s %s\n", job.id, job.job_name); + printf(" state: %s", state ? state : job.state); + if (job.updated_at[0]) printf(" updated: %s", job.updated_at); + printf("\n"); + + if (job.printer_uri[0]) printf(" printer: %s\n", job.printer_uri); + if (job.doc_format[0]) printf(" format: %s\n", job.doc_format); + if (job.source_name[0]) printf(" source: %s\n", job.source_name); + if (job.remote_job_id > 0) printf(" printer job id: %d\n", job.remote_job_id); + if (job.status_message[0]) printf(" status: %s\n", job.status_message); + if ((verbose || strcmp(state, "failed") == 0 || strcmp(state, "printing") == 0) && + job.debug_info[0]) { + printf(" debug: %s\n", job.debug_info); + } +} + +static void print_job_line(const char* dir, const char* label, bool verbose) { + DIR* d = opendir(dir); + if (!d) { + printf("%s: unavailable\n", label); + return; + } + + printf("%s:\n", label); + int count = 0; + struct dirent* ent = nullptr; + while ((ent = readdir(d)) != nullptr) { + if (ent->d_name[0] == '.') continue; + char path[MAX_PATH_LEN]; + path_join(path, sizeof(path), dir, ent->d_name); + JobMeta job = {}; + if (!load_job_from_path(path, &job)) continue; + const char* state = strcmp(label, "Queued") == 0 ? "queued" + : strcmp(label, "Active") == 0 ? "printing" + : strcmp(label, "Done") == 0 ? "completed" + : strcmp(label, "Failed") == 0 ? "failed" + : job.state; + print_job_detail(job, state, verbose); + count++; + } + closedir(d); + + if (count == 0) printf(" empty\n"); +} + +static int cmd_status(const ArgList& args) { + ensure_spool_dirs(); + bool verbose = false; + for (int i = 1; i < args.count; i++) { + if (strcmp(args.argv[i], "--verbose") == 0) verbose = true; + else { + usage(); + return 1; + } + } + + char uri[MAX_PATH_LEN]; + if (read_default_printer_uri(uri, sizeof(uri))) { + printf("Default printer: %s\n", uri); + if (verbose) { + char err[128] = {}; + IppUri parsed = {}; + if (normalize_ipp_uri(uri, &parsed, err, sizeof(err))) { + resolve_host(parsed.host, &parsed.ip); + print_host_details(parsed); + } else { + printf("Printer URI error: %s\n", err); + } + } + } else + printf("Default printer: not configured\n"); + + int pid = -1; + if (daemon_is_running(&pid)) + printf("Daemon: running (pid %d)\n", pid); + else + printf("Daemon: stopped\n"); + + print_job_line(SPOOL_QUEUE_DIR, "Queued", verbose); + print_job_line(SPOOL_ACTIVE_DIR, "Active", verbose); + print_job_line(SPOOL_DONE_DIR, "Done", verbose); + print_job_line(SPOOL_FAILED_DIR, "Failed", verbose); + return 0; +} + +static int cmd_set_printer(const char* uri) { + char err[128]; + IppUri parsed = {}; + if (!normalize_ipp_uri(uri, &parsed, err, sizeof(err))) { + printf("printctl: %s\n", err); + return 1; + } + if (!write_default_printer_uri(parsed.normalized)) { + printf("printctl: failed to write default printer\n"); + return 1; + } + printf("%s\n", parsed.normalized); + return 0; +} + +static int print_submission_result(const char* job_id, bool wait) { + printf("queued job %s\n", job_id); + if (!wait) return 0; + + JobMeta job = {}; + char err[128] = {}; + bool ok = wait_for_job(job_id, &job, DEFAULT_WAIT_TIMEOUT_MS, err, sizeof(err)); + if (job.id[0] != '\0') { + if (ok) { + printf("completed: %s\n", job.status_message[0] ? job.status_message : "Printed"); + if (job.remote_job_id > 0) printf("printer job id: %d\n", job.remote_job_id); + if (job.debug_info[0]) printf("detail: %s\n", job.debug_info); + return 0; + } + printf("failed: %s\n", job.status_message[0] ? job.status_message : "Print failed"); + if (job.debug_info[0]) printf("detail: %s\n", job.debug_info); + return 1; + } + + printf("printctl: %s\n", err[0] ? err : "timed out waiting for job"); + char path[MAX_PATH_LEN]; + char state[16]; + if (find_job(job_id, path, sizeof(path), state, sizeof(state))) { + JobMeta current = {}; + if (load_job_from_path(path, ¤t)) { + printf("current state: %s\n", state); + if (current.status_message[0]) printf("status: %s\n", current.status_message); + if (current.debug_info[0]) printf("detail: %s\n", current.debug_info); + } + } + return 1; +} + +static int cmd_print(const ArgList& args) { + const char* file = nullptr; + const char* printer = nullptr; + const char* name = nullptr; + bool wait = false; + + for (int i = 1; i < args.count; i++) { + if (strcmp(args.argv[i], "--printer") == 0 && i + 1 < args.count) { + printer = args.argv[++i]; + } else if (strcmp(args.argv[i], "--name") == 0 && i + 1 < args.count) { + name = args.argv[++i]; + } else if (strcmp(args.argv[i], "--wait") == 0) { + wait = true; + } else if (file == nullptr) { + file = args.argv[i]; + } else { + usage(); + return 1; + } + } + + if (file == nullptr) { + usage(); + return 1; + } + + char job_id[48]; + char err[128] = {}; + if (!submit_document_job(file, printer, name, job_id, sizeof(job_id), err, sizeof(err))) { + printf("printctl: %s\n", err); + return 1; + } + + return print_submission_result(job_id, wait); +} + +static int cmd_test_page(const ArgList& args) { + const char* printer = nullptr; + bool wait = true; + + for (int i = 1; i < args.count; i++) { + if (strcmp(args.argv[i], "--printer") == 0 && i + 1 < args.count) { + printer = args.argv[++i]; + } else if (strcmp(args.argv[i], "--wait") == 0) { + wait = true; + } else if (strcmp(args.argv[i], "--no-wait") == 0) { + wait = false; + } else { + usage(); + return 1; + } + } + + char job_id[48]; + char err[128] = {}; + if (!submit_test_page_job(printer, job_id, sizeof(job_id), err, sizeof(err))) { + printf("printctl: %s\n", err); + return 1; + } + return print_submission_result(job_id, wait); +} + +static int cmd_inspect(const char* job_id) { + char path[MAX_PATH_LEN]; + char state[16]; + if (!find_job(job_id, path, sizeof(path), state, sizeof(state))) { + printf("printctl: job %s not found\n", job_id); + return 1; + } + + JobMeta job = {}; + if (!load_job_from_path(path, &job)) { + printf("printctl: failed to load job %s\n", job_id); + return 1; + } + + printf("Job ID: %s\n", job.id); + printf("State: %s\n", state); + printf("Name: %s\n", job.job_name); + if (job.user_name[0]) printf("User: %s\n", job.user_name); + if (job.printer_uri[0]) printf("Printer: %s\n", job.printer_uri); + if (job.doc_format[0]) printf("Format: %s\n", job.doc_format); + if (job.source_name[0]) printf("Source: %s\n", job.source_name); + if (job.created_at[0]) printf("Created: %s\n", job.created_at); + if (job.updated_at[0]) printf("Updated: %s\n", job.updated_at); + if (job.remote_job_id > 0) printf("Printer job id: %d\n", job.remote_job_id); + if (job.status_message[0]) printf("Status: %s\n", job.status_message); + if (job.debug_info[0]) printf("Debug: %s\n", job.debug_info); + return 0; +} + +static int cmd_probe(const char* printer_uri) { + char uri_buf[MAX_PATH_LEN]; + if (printer_uri && *printer_uri) { + safe_copy(uri_buf, sizeof(uri_buf), printer_uri); + } else if (!read_default_printer_uri(uri_buf, sizeof(uri_buf))) { + printf("printctl: no default printer is configured\n"); + return 1; + } + + char err[128] = {}; + IppUri normalized = {}; + if (!normalize_ipp_uri(uri_buf, &normalized, err, sizeof(err))) { + printf("printctl: %s\n", err); + return 1; + } + + printf("Printer URI: %s\n", normalized.normalized); + print_host_details(normalized); + + uint32_t ip = 0; + if (!resolve_host(normalized.host, &ip)) { + if (host_looks_like_mdns(normalized.host)) + printf("Resolution: unresolved (.local/mDNS not supported yet)\n"); + else + printf("Resolution: unresolved\n"); + return 1; + } + + normalized.ip = ip; + char ip_text[20]; + format_ipv4(ip_text, sizeof(ip_text), ip); + printf("Resolution: ok (%s)\n", ip_text); + + IppCapabilities caps = {}; + if (!ipp_get_printer_capabilities(normalized.normalized, &caps, err, sizeof(err))) { + char detail[256] = {}; + summarize_ipp_capabilities(&caps, detail, sizeof(detail)); + printf("Probe failed: %s\n", err[0] ? err : "IPP probe failed"); + if (detail[0]) printf("Detail: %s\n", detail); + return 1; + } + + char detail[256] = {}; + summarize_ipp_capabilities(&caps, detail, sizeof(detail)); + printf("Probe: ok\n"); + if (caps.printer_name[0]) printf("Printer name: %s\n", caps.printer_name); + printf("Supports PDF: %s\n", caps.supports_pdf ? "yes" : "no"); + printf("Supports JPEG: %s\n", caps.supports_jpeg ? "yes" : "no"); + printf("Supports text: %s\n", caps.supports_text ? "yes" : "no"); + if (caps.preferred_format[0]) printf("Preferred format: %s\n", caps.preferred_format); + if (caps.default_format[0]) printf("Default format: %s\n", caps.default_format); + if (caps.supported_formats[0]) printf("Supported formats: %s\n", caps.supported_formats); + if (caps.status_message[0]) printf("Printer status: %s\n", caps.status_message); + if (detail[0]) printf("Detail: %s\n", detail); + return 0; +} + +extern "C" void _start() { + ArgList args = {}; + parse_args(&args); + if (args.count <= 0) { + usage(); + montauk::exit(1); + } + + int rc = 1; + if (strcmp(args.argv[0], "set-printer") == 0) { + if (args.count < 2) usage(); + else rc = cmd_set_printer(args.argv[1]); + } else if (strcmp(args.argv[0], "show-printer") == 0) { + char uri[MAX_PATH_LEN]; + if (read_default_printer_uri(uri, sizeof(uri))) { + printf("%s\n", uri); + rc = 0; + } else { + printf("printctl: no default printer is configured\n"); + rc = 1; + } + } else if (strcmp(args.argv[0], "print") == 0) { + rc = cmd_print(args); + } else if (strcmp(args.argv[0], "test-page") == 0) { + rc = cmd_test_page(args); + } else if (strcmp(args.argv[0], "status") == 0) { + rc = cmd_status(args); + } else if (strcmp(args.argv[0], "inspect") == 0) { + if (args.count < 2) usage(); + else rc = cmd_inspect(args.argv[1]); + } else if (strcmp(args.argv[0], "probe") == 0) { + rc = cmd_probe(args.count >= 2 ? args.argv[1] : nullptr); + } else { + usage(); + rc = 1; + } + + montauk::exit(rc); +} diff --git a/programs/src/printd/Makefile b/programs/src/printd/Makefile new file mode 100644 index 0000000..0f66ffe --- /dev/null +++ b/programs/src/printd/Makefile @@ -0,0 +1,79 @@ +# Makefile for printd (userspace print spooler) on MontaukOS + +MAKEFLAGS += -rR +.SUFFIXES: + +TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf- +ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),) + CXX := $(TOOLCHAIN_PREFIX)g++ +else + CXX := g++ +endif + +PROG_INC := ../../include +LIBC_LIB := ../../lib/libc +TLS_LIB := ../../lib/tls +BEARSSL := ../../lib/bearssl +JPEGWRITE := ../../lib/libjpegwrite +LINK_LD := ../../link.ld +BINDIR := ../../bin +OBJDIR := obj + +CXXFLAGS := \ + -std=gnu++20 \ + -g -O2 -pipe \ + -Wall \ + -Wextra \ + -Wno-unused-parameter \ + -nostdinc \ + -ffreestanding \ + -fno-stack-protector \ + -fno-stack-check \ + -fno-PIC \ + -fno-rtti \ + -fno-exceptions \ + -ffunction-sections \ + -fdata-sections \ + -m64 \ + -march=x86-64 \ + -msse \ + -msse2 \ + -mno-red-zone \ + -mcmodel=small \ + -I $(PROG_INC) \ + -isystem $(PROG_INC)/libc \ + -I $(BEARSSL)/inc \ + -isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \ + -isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include + +LDFLAGS := \ + -nostdlib \ + -static \ + -Wl,--build-id=none \ + -Wl,--gc-sections \ + -Wl,-m,elf_x86_64 \ + -z max-page-size=0x1000 \ + -T $(LINK_LD) + +SRCS := main.cpp test_page_jpeg.cpp font_data.cpp stb_truetype_impl.cpp +OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) +LIBS := $(TLS_LIB)/libtls.a $(BEARSSL)/libbearssl.a $(JPEGWRITE)/libjpegwrite.a $(LIBC_LIB)/liblibc.a +TARGET := $(BINDIR)/os/printd.elf +DEPS := $(OBJS:.o=.d) + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): $(OBJS) $(LIBS) $(LINK_LD) Makefile + mkdir -p $(BINDIR)/os + $(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@ + +$(OBJDIR)/%.o: %.cpp Makefile + mkdir -p $(OBJDIR) + $(CXX) $(CXXFLAGS) -MMD -MP -c $< -o $@ + +-include $(DEPS) + +clean: + rm -rf $(OBJDIR) $(TARGET) diff --git a/programs/src/printd/font_data.cpp b/programs/src/printd/font_data.cpp new file mode 100644 index 0000000..a591f69 --- /dev/null +++ b/programs/src/printd/font_data.cpp @@ -0,0 +1 @@ +#include "../desktop/font_data.cpp" diff --git a/programs/src/printd/main.cpp b/programs/src/printd/main.cpp new file mode 100644 index 0000000..f72b269 --- /dev/null +++ b/programs/src/printd/main.cpp @@ -0,0 +1,273 @@ +/* + * main.cpp + * MontaukOS userspace print spooler daemon + * Copyright (c) 2026 Daniel Hammer + */ + +#include +#include +#include "test_page_jpeg.hpp" + +extern "C" { +#include +#include +} + +using namespace print; + +static void log_msg(const char* fmt, ...) { + va_list ap; + va_start(ap, fmt); + printf("[printd] "); + vprintf(fmt, ap); + printf("\n"); + va_end(ap); +} + +static void set_job_state(JobMeta* job, const char* state, const char* message) { + safe_copy(job->state, sizeof(job->state), state); + if (message && *message) safe_copy(job->status_message, sizeof(job->status_message), message); + now_string(job->updated_at, sizeof(job->updated_at)); +} + +static void set_job_debug(JobMeta* job, const char* detail) { + if (job == nullptr) return; + safe_copy(job->debug_info, sizeof(job->debug_info), detail ? detail : ""); +} + +static void set_job_debug_from_result(JobMeta* job, const IppPrintResult* result, const char* prefix) { + if (job == nullptr) return; + char detail[MAX_DEBUG_LEN] = {}; + summarize_ipp_print_result(result, detail, sizeof(detail)); + if (prefix && *prefix) { + if (detail[0]) { + char joined[MAX_DEBUG_LEN]; + snprintf(joined, sizeof(joined), "%s; %s", prefix, detail); + safe_copy(job->debug_info, sizeof(job->debug_info), joined); + } else { + safe_copy(job->debug_info, sizeof(job->debug_info), prefix); + } + } else { + safe_copy(job->debug_info, sizeof(job->debug_info), detail); + } +} + +static void persist_job_debug(const char* active_path, JobMeta* job, const char* detail) { + if (active_path == nullptr || *active_path == '\0' || job == nullptr) return; + set_job_debug(job, detail); + now_string(job->updated_at, sizeof(job->updated_at)); + save_job_to_path_atomic(active_path, job); +} + +static bool write_pid_file() { + char pid[32]; + snprintf(pid, sizeof(pid), "%d\n", montauk::getpid()); + return write_text_file_atomic(SPOOL_DAEMON_PID_PATH, pid); +} + +static void requeue_stale_active_jobs() { + DIR* dir = opendir(SPOOL_ACTIVE_DIR); + if (!dir) return; + + struct dirent* ent = nullptr; + while ((ent = readdir(dir)) != nullptr) { + if (ent->d_name[0] == '.') continue; + + char src[MAX_PATH_LEN]; + char dst[MAX_PATH_LEN]; + path_join(src, sizeof(src), SPOOL_ACTIVE_DIR, ent->d_name); + path_join(dst, sizeof(dst), SPOOL_QUEUE_DIR, ent->d_name); + + JobMeta job = {}; + if (load_job_from_path(src, &job)) { + set_job_state(&job, "queued", "Re-queued after daemon restart"); + save_job_to_path_atomic(src, &job); + } + rename(src, dst); + } + closedir(dir); +} + +static bool claim_next_job(char* out_active_path, int out_active_path_len) { + DIR* dir = opendir(SPOOL_QUEUE_DIR); + if (!dir) return false; + + bool claimed = false; + struct dirent* ent = nullptr; + while ((ent = readdir(dir)) != nullptr) { + if (ent->d_name[0] == '.') continue; + + char queued[MAX_PATH_LEN]; + char active[MAX_PATH_LEN]; + path_join(queued, sizeof(queued), SPOOL_QUEUE_DIR, ent->d_name); + path_join(active, sizeof(active), SPOOL_ACTIVE_DIR, ent->d_name); + + if (rename(queued, active) == 0) { + safe_copy(out_active_path, out_active_path_len, active); + claimed = true; + break; + } + } + + closedir(dir); + return claimed; +} + +static void finalize_job(const char* active_path, const char* target_dir, JobMeta* job) { + save_job_to_path_atomic(active_path, job); + char target[MAX_PATH_LEN]; + make_job_file_path(target_dir, job->id, target, sizeof(target)); + if (rename(active_path, target) != 0) { + log_msg("warning: failed to move job %s to %s", job->id, target_dir); + } +} + +static bool try_print_test_page(const char* active_path, JobMeta* job, + IppPrintResult* result, char* err, int err_len) { + persist_job_debug(active_path, job, "querying printer capabilities"); + + IppCapabilities caps = {}; + bool have_caps = ipp_get_printer_capabilities(job->printer_uri, &caps, err, err_len); + if (have_caps) { + char detail[MAX_DEBUG_LEN] = {}; + summarize_ipp_capabilities(&caps, detail, sizeof(detail)); + persist_job_debug(active_path, job, detail); + } else if (err && err[0]) { + char detail[MAX_DEBUG_LEN]; + snprintf(detail, sizeof(detail), "capability probe failed; attempting direct submit (%s)", err); + persist_job_debug(active_path, job, detail); + } + + const struct { + const char* mime; + bool (*generate)(uint8_t**, int*); + bool allowed; + } attempts[] = { + {"application/pdf", generate_test_page_pdf, !have_caps || caps.supports_pdf}, + {"image/jpeg", generate_test_page_jpeg, !have_caps || caps.supports_jpeg}, + {"text/plain", generate_test_page_text, !have_caps || caps.supports_text}, + }; + + if (have_caps && !caps.supports_pdf && !caps.supports_jpeg && !caps.supports_text) { + if (caps.supported_formats[0]) { + snprintf(err, (size_t)err_len, + "printer does not advertise a built-in test-page format (supports: %s)", + caps.supported_formats); + } else { + safe_copy(err, err_len, "printer does not advertise a built-in test-page format"); + } + return false; + } + + for (size_t i = 0; i < sizeof(attempts) / sizeof(attempts[0]); i++) { + if (!attempts[i].allowed) continue; + + uint8_t* data = nullptr; + int len = 0; + if (!attempts[i].generate(&data, &len)) { + safe_copy(err, err_len, "failed to generate test page"); + return false; + } + + char detail[MAX_DEBUG_LEN]; + snprintf(detail, sizeof(detail), "submitting test page as %s (%d bytes)", + attempts[i].mime, len); + persist_job_debug(active_path, job, detail); + + bool ok = ipp_print_buffer(job->printer_uri, job->job_name, job->user_name, + attempts[i].mime, data, len, result, err, err_len); + free(data); + if (ok) return true; + } + + return false; +} + +static bool try_print_document(const char* active_path, JobMeta* job, + IppPrintResult* result, char* err, int err_len) { + persist_job_debug(active_path, job, "loading document bytes"); + + uint8_t* data = nullptr; + int len = 0; + if (!read_file_bytes(job->doc_path, &data, &len, err, err_len)) return false; + + char detail[MAX_DEBUG_LEN]; + snprintf(detail, sizeof(detail), "submitting %s document (%d bytes)", + job->doc_format[0] ? job->doc_format : "application/octet-stream", len); + persist_job_debug(active_path, job, detail); + + bool ok = ipp_print_buffer(job->printer_uri, job->job_name, job->user_name, + job->doc_format, data, len, result, err, err_len); + free(data); + return ok; +} + +static void process_job(const char* active_path) { + JobMeta job = {}; + if (!load_job_from_path(active_path, &job)) { + log_msg("dropping unreadable job file: %s", active_path); + remove(active_path); + return; + } + + log_msg("processing job %s (%s)", job.id, job.job_name); + set_job_state(&job, "printing", "Sending to printer"); + set_job_debug(&job, "dispatching queued job"); + save_job_to_path_atomic(active_path, &job); + + IppPrintResult result = {}; + char err[128] = {}; + bool ok = false; + + if (strcmp(job.source_kind, "test-page") == 0) + ok = try_print_test_page(active_path, &job, &result, err, sizeof(err)); + else + ok = try_print_document(active_path, &job, &result, err, sizeof(err)); + + if (ok) { + job.remote_job_id = result.job_id; + set_job_state(&job, "completed", + result.status_message[0] ? result.status_message : "Printed"); + set_job_debug_from_result(&job, &result, nullptr); + finalize_job(active_path, SPOOL_DONE_DIR, &job); + log_msg("job %s completed (printer job id %d)", job.id, result.job_id); + } else { + set_job_state(&job, "failed", err[0] ? err : "Print failed"); + set_job_debug_from_result(&job, &result, err[0] ? err : nullptr); + finalize_job(active_path, SPOOL_FAILED_DIR, &job); + log_msg("job %s failed: %s", job.id, job.status_message); + if (job.debug_info[0]) log_msg("job %s debug: %s", job.id, job.debug_info); + } + + if (job.doc_path[0] != '\0') remove(job.doc_path); +} + +extern "C" void _start() { + if (!ensure_spool_dirs()) { + log_msg("failed to initialize spool directories"); + montauk::exit(1); + } + + int running_pid = -1; + if (daemon_is_running(&running_pid) && running_pid != montauk::getpid()) { + log_msg("another print daemon is already running (pid %d)", running_pid); + montauk::exit(0); + } + + if (!write_pid_file()) { + log_msg("failed to write daemon pid file"); + montauk::exit(1); + } + + requeue_stale_active_jobs(); + log_msg("ready"); + + for (;;) { + char active_path[MAX_PATH_LEN]; + if (claim_next_job(active_path, sizeof(active_path))) { + process_job(active_path); + continue; + } + montauk::sleep_ms(1000); + } +} diff --git a/programs/src/printd/stb_truetype_impl.cpp b/programs/src/printd/stb_truetype_impl.cpp new file mode 100644 index 0000000..80309db --- /dev/null +++ b/programs/src/printd/stb_truetype_impl.cpp @@ -0,0 +1,32 @@ +/* + * stb_truetype_impl.cpp + * Single compilation unit for stb_truetype in MontaukOS freestanding environment + */ + +#include +#include +#include +#include +#include + +#define STBTT_ifloor(x) ((int) stb_floor(x)) +#define STBTT_iceil(x) ((int) stb_ceil(x)) +#define STBTT_sqrt(x) stb_sqrt(x) +#define STBTT_pow(x,y) stb_pow(x,y) +#define STBTT_fmod(x,y) stb_fmod(x,y) +#define STBTT_cos(x) stb_cos(x) +#define STBTT_acos(x) stb_acos(x) +#define STBTT_fabs(x) stb_fabs(x) + +#define STBTT_malloc(x,u) ((void)(u), montauk::malloc(x)) +#define STBTT_free(x,u) ((void)(u), montauk::mfree(x)) + +#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n) +#define STBTT_memset(d,v,n) montauk::memset(d,v,n) + +#define STBTT_strlen(x) montauk::slen(x) + +#define STBTT_assert(x) ((void)(x)) + +#define STB_TRUETYPE_IMPLEMENTATION +#include diff --git a/programs/src/printd/test_page_jpeg.cpp b/programs/src/printd/test_page_jpeg.cpp new file mode 100644 index 0000000..d5b28d3 --- /dev/null +++ b/programs/src/printd/test_page_jpeg.cpp @@ -0,0 +1,311 @@ +/* + * test_page_jpeg.cpp + * JPEG test-page generator for printers that do not accept PDF/text jobs. + */ + +#include "test_page_jpeg.hpp" + +#include +#include +#include + +extern "C" { +#include +#include +#include + +#define STBI_WRITE_NO_STDIO +#include +} + +namespace gui { +extern const uint8_t font_data[256 * 16]; +} + +namespace { + +static constexpr int PAGE_W = 1240; +static constexpr int PAGE_H = 1754; +static constexpr const char* ROBOTO_MEDIUM_PATH = "0:/fonts/Roboto-Medium.ttf"; +static constexpr const char* ROBOTO_BOLD_PATH = "0:/fonts/Roboto-Bold.ttf"; + +struct JpegBuffer { + uint8_t* data; + int size; + int capacity; + bool failed; +}; + +struct PageFonts { + gui::TrueTypeFont* medium; + gui::TrueTypeFont* bold; +}; + +static void jpeg_write_callback(void* context, void* data, int size) { + JpegBuffer* buf = (JpegBuffer*)context; + if (!buf || !data || size <= 0 || buf->failed) return; + + int needed = buf->size + size; + if (needed > buf->capacity) { + int new_cap = buf->capacity > 0 ? buf->capacity * 2 : 4096; + if (new_cap < needed) new_cap = needed; + uint8_t* new_data = (uint8_t*)realloc(buf->data, (size_t)new_cap); + if (!new_data) { + buf->failed = true; + return; + } + buf->data = new_data; + buf->capacity = new_cap; + } + + memcpy(buf->data + buf->size, data, (size_t)size); + buf->size += size; +} + +static void fill_rect(uint8_t* rgb, int x, int y, int w, int h, uint8_t r, uint8_t g, uint8_t b) { + if (!rgb || w <= 0 || h <= 0) return; + if (x < 0) { w += x; x = 0; } + if (y < 0) { h += y; y = 0; } + if (x + w > PAGE_W) w = PAGE_W - x; + if (y + h > PAGE_H) h = PAGE_H - y; + if (w <= 0 || h <= 0) return; + + for (int yy = y; yy < y + h; yy++) { + uint8_t* row = rgb + (yy * PAGE_W + x) * 3; + for (int xx = 0; xx < w; xx++) { + row[xx * 3 + 0] = r; + row[xx * 3 + 1] = g; + row[xx * 3 + 2] = b; + } + } +} + +static void draw_rect_outline(uint8_t* rgb, int x, int y, int w, int h, + int thickness, uint8_t r, uint8_t g, uint8_t b) { + fill_rect(rgb, x, y, w, thickness, r, g, b); + fill_rect(rgb, x, y + h - thickness, w, thickness, r, g, b); + fill_rect(rgb, x, y, thickness, h, r, g, b); + fill_rect(rgb, x + w - thickness, y, thickness, h, r, g, b); +} + +static void blend_pixel(uint8_t* rgb, int x, int y, + uint8_t r, uint8_t g, uint8_t b, uint8_t alpha) { + if (!rgb || alpha == 0 || x < 0 || x >= PAGE_W || y < 0 || y >= PAGE_H) return; + uint8_t* p = rgb + (y * PAGE_W + x) * 3; + if (alpha == 255) { + p[0] = r; + p[1] = g; + p[2] = b; + return; + } + + uint32_t a = alpha; + uint32_t inv = 255 - a; + p[0] = (uint8_t)((a * r + inv * p[0] + 127) / 255); + p[1] = (uint8_t)((a * g + inv * p[1] + 127) / 255); + p[2] = (uint8_t)((a * b + inv * p[2] + 127) / 255); +} + +static void draw_char_scaled(uint8_t* rgb, int x, int y, int scale, char ch, + uint8_t r, uint8_t g, uint8_t b) { + const uint8_t* glyph = &gui::font_data[(unsigned char)ch * 16]; + for (int row = 0; row < 16; row++) { + uint8_t bits = glyph[row]; + for (int col = 0; col < 8; col++) { + if (!(bits & (0x80 >> col))) continue; + fill_rect(rgb, x + col * scale, y + row * scale, scale, scale, r, g, b); + } + } +} + +static void draw_text_scaled(uint8_t* rgb, int x, int y, int scale, const char* text, + uint8_t r, uint8_t g, uint8_t b) { + if (!rgb || !text) return; + for (int i = 0; text[i] != '\0'; i++) + draw_char_scaled(rgb, x + i * 8 * scale, y, scale, text[i], r, g, b); +} + +static bool init_page_font(gui::TrueTypeFont* font, const char* path) { + if (!font || !path) return false; + memset(font, 0, sizeof(*font)); + return font->init(path); +} + +static gui::TrueTypeFont* load_page_font(const char* path) { + gui::TrueTypeFont* font = (gui::TrueTypeFont*)montauk::malloc(sizeof(gui::TrueTypeFont)); + if (!font) return nullptr; + if (!init_page_font(font, path)) { + montauk::mfree(font); + return nullptr; + } + return font; +} + +static void free_page_font(gui::TrueTypeFont* font) { + if (!font) return; + for (int i = 0; i < font->cache_count; i++) { + for (int g = 0; g < 256; g++) { + if (font->caches[i].glyphs[g].bitmap) { + montauk::mfree(font->caches[i].glyphs[g].bitmap); + font->caches[i].glyphs[g].bitmap = nullptr; + } + } + } + if (font->data) { + montauk::free(font->data); + font->data = nullptr; + } + montauk::mfree(font); +} + +static void destroy_page_fonts(PageFonts* fonts) { + if (!fonts) return; + free_page_font(fonts->medium); + free_page_font(fonts->bold); + fonts->medium = nullptr; + fonts->bold = nullptr; +} + +static PageFonts load_page_fonts() { + PageFonts fonts = {}; + fonts.medium = load_page_font(ROBOTO_MEDIUM_PATH); + fonts.bold = load_page_font(ROBOTO_BOLD_PATH); + return fonts; +} + +static gui::TrueTypeFont* pick_font(PageFonts* fonts, bool bold) { + if (!fonts) return nullptr; + if (bold && fonts->bold && fonts->bold->valid) return fonts->bold; + if (fonts->medium && fonts->medium->valid) return fonts->medium; + if (fonts->bold && fonts->bold->valid) return fonts->bold; + return nullptr; +} + +static int measure_text(uint8_t* rgb, PageFonts* fonts, bool bold, + int pixel_size, int fallback_scale, const char* text) { + (void)rgb; + if (!text) return 0; + gui::TrueTypeFont* font = pick_font(fonts, bold); + if (font && font->valid) return font->measure_text(text, pixel_size); + return (int)strlen(text) * 8 * fallback_scale; +} + +static void draw_text(uint8_t* rgb, PageFonts* fonts, bool bold, + int x, int y, int pixel_size, int fallback_scale, + const char* text, uint8_t r, uint8_t g, uint8_t b) { + if (!rgb || !text) return; + + gui::TrueTypeFont* font = pick_font(fonts, bold); + if (!font || !font->valid) { + draw_text_scaled(rgb, x, y, fallback_scale, text, r, g, b); + return; + } + + gui::GlyphCache* gc = font->get_cache(pixel_size); + if (!gc) { + draw_text_scaled(rgb, x, y, fallback_scale, text, r, g, b); + return; + } + + int cx = x; + int baseline = y + gc->ascent; + for (int i = 0; text[i] != '\0'; i++) { + gui::CachedGlyph* glyph = font->get_glyph(gc, (unsigned char)text[i]); + if (!glyph) continue; + + if (glyph->bitmap) { + int gx = cx + glyph->xoff; + int gy = baseline + glyph->yoff; + for (int row = 0; row < glyph->height; row++) { + for (int col = 0; col < glyph->width; col++) { + uint8_t alpha = glyph->bitmap[row * glyph->width + col]; + if (alpha == 0) continue; + blend_pixel(rgb, gx + col, gy + row, r, g, b, alpha); + } + } + } + cx += glyph->advance; + } +} + +static void draw_centered_text(uint8_t* rgb, PageFonts* fonts, bool bold, + int y, int pixel_size, int fallback_scale, + const char* text, uint8_t r, uint8_t g, uint8_t b) { + if (!text) return; + int width = measure_text(rgb, fonts, bold, pixel_size, fallback_scale, text); + int x = (PAGE_W - width) / 2; + draw_text(rgb, fonts, bold, x, y, pixel_size, fallback_scale, text, r, g, b); +} + +} // namespace + +bool generate_test_page_jpeg(uint8_t** out_data, int* out_len) { + if (out_data) *out_data = nullptr; + if (out_len) *out_len = 0; + + uint8_t* rgb = (uint8_t*)malloc((size_t)PAGE_W * PAGE_H * 3); + if (!rgb) return false; + memset(rgb, 0xFF, (size_t)PAGE_W * PAGE_H * 3); + + PageFonts fonts = load_page_fonts(); + + draw_centered_text(rgb, &fonts, true, 50, 52, 3, + "Montauk Operating System", 0x22, 0x22, 0x22); + draw_centered_text(rgb, &fonts, false, 112, 26, 2, + "Printer Test Page for IPP Printers", 0x66, 0x66, 0x66); + fill_rect(rgb, 120, 170, PAGE_W - 240, 4, 0x29, 0x8F, 0x45); + + fill_rect(rgb, 80, 220, PAGE_W - 160, 240, 0xFA, 0xFA, 0xFA); + draw_rect_outline(rgb, 80, 220, PAGE_W - 160, 240, 3, 0xD9, 0xD9, 0xD9); + + char timestamp[32]; + print::now_string(timestamp, sizeof(timestamp)); + + char line[128]; + snprintf(line, sizeof(line), "Generated: %s", timestamp); + draw_text(rgb, &fonts, false, 120, 268, 30, 2, line, 0x22, 0x22, 0x22); + draw_text(rgb, &fonts, false, 120, 320, 30, 2, "Format: image/jpeg", 0x22, 0x22, 0x22); + draw_text(rgb, &fonts, false, 120, 372, 30, 2, "If you can read this, printing works.", 0x22, 0x22, 0x22); + + draw_text(rgb, &fonts, true, 100, 530, 32, 2, "Color Check", 0x22, 0x22, 0x22); + draw_rect_outline(rgb, 100, 590, PAGE_W - 200, 150, 3, 0xD0, 0xD0, 0xD0); + fill_rect(rgb, 130, 625, 150, 72, 0x30, 0x30, 0x30); + fill_rect(rgb, 320, 625, 150, 72, 0x25, 0xA8, 0xD8); + fill_rect(rgb, 510, 625, 150, 72, 0xD8, 0x3C, 0x82); + fill_rect(rgb, 700, 625, 150, 72, 0xE9, 0xCF, 0x4A); + fill_rect(rgb, 890, 625, 150, 72, 0x47, 0x98, 0x5E); + draw_text(rgb, &fonts, false, 150, 706, 18, 1, "Black", 0x33, 0x33, 0x33); + draw_text(rgb, &fonts, false, 352, 706, 18, 1, "Cyan", 0x33, 0x33, 0x33); + draw_text(rgb, &fonts, false, 518, 706, 18, 1, "Magenta", 0x33, 0x33, 0x33); + draw_text(rgb, &fonts, false, 715, 706, 18, 1, "Yellow", 0x33, 0x33, 0x33); + draw_text(rgb, &fonts, false, 932, 706, 18, 1, "Green", 0x33, 0x33, 0x33); + + draw_text(rgb, &fonts, true, 100, 792, 32, 2, "Grayscale", 0x22, 0x22, 0x22); + for (int i = 0; i < 10; i++) { + uint8_t v = (uint8_t)(i * 255 / 9); + fill_rect(rgb, 100 + i * ((PAGE_W - 200) / 10), 855, + (PAGE_W - 200) / 10, 72, v, v, v); + } + draw_rect_outline(rgb, 100, 855, PAGE_W - 200, 72, 3, 0xC8, 0xC8, 0xC8); + + draw_text(rgb, &fonts, true, 100, 998, 32, 2, "Alignment", 0x22, 0x22, 0x22); + for (int i = 0; i < 5; i++) { + int yy = 1070 + i * 60; + fill_rect(rgb, 100, yy, PAGE_W - 200, 3, 0x11, 0x11, 0x11); + fill_rect(rgb, 100 + i * 40, yy - 15, 3, 33, 0x11, 0x11, 0x11); + } + + JpegBuffer jpeg = {}; + int ok = stbi_write_jpg_to_func(jpeg_write_callback, &jpeg, PAGE_W, PAGE_H, 3, rgb, 82); + destroy_page_fonts(&fonts); + free(rgb); + if (!ok || jpeg.failed || !jpeg.data || jpeg.size <= 0) { + free(jpeg.data); + return false; + } + + if (out_data) *out_data = jpeg.data; + else free(jpeg.data); + if (out_len) *out_len = jpeg.size; + return true; +} diff --git a/programs/src/printd/test_page_jpeg.hpp b/programs/src/printd/test_page_jpeg.hpp new file mode 100644 index 0000000..c81f590 --- /dev/null +++ b/programs/src/printd/test_page_jpeg.hpp @@ -0,0 +1,5 @@ +#pragma once + +#include + +bool generate_test_page_jpeg(uint8_t** out_data, int* out_len); diff --git a/programs/src/printers/Makefile b/programs/src/printers/Makefile new file mode 100644 index 0000000..9e66697 --- /dev/null +++ b/programs/src/printers/Makefile @@ -0,0 +1,79 @@ +# Makefile for printers (standalone Window Server app) on MontaukOS +# Copyright (c) 2026 Daniel Hammer + +MAKEFLAGS += -rR +.SUFFIXES: + +TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf- +ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),) + CXX := $(TOOLCHAIN_PREFIX)g++ +else + CXX := g++ +endif + +PROG_INC := ../../include +LINK_LD := ../../link.ld +BINDIR := ../../bin +OBJDIR := obj +LIBDIR := ../../lib + +CXXFLAGS := \ + -std=gnu++20 \ + -g -O2 -pipe \ + -Wall \ + -Wextra \ + -Wno-unused-parameter \ + -Wno-unused-function \ + -nostdinc \ + -ffreestanding \ + -fno-stack-protector \ + -fno-stack-check \ + -fno-PIC \ + -fno-rtti \ + -fno-exceptions \ + -ffunction-sections \ + -fdata-sections \ + -m64 \ + -march=x86-64 \ + -msse \ + -msse2 \ + -mno-red-zone \ + -mcmodel=small \ + -MMD -MP \ + -I $(PROG_INC) \ + -isystem $(PROG_INC)/libc \ + -I ../../lib/bearssl/inc \ + -isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \ + -isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include + +LDFLAGS := \ + -nostdlib \ + -static \ + -Wl,--build-id=none \ + -Wl,--gc-sections \ + -Wl,-m,elf_x86_64 \ + -z max-page-size=0x1000 \ + -T $(LINK_LD) + +SRCS := main.cpp stb_truetype_impl.cpp font_data.cpp +OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) + +TARGET := $(BINDIR)/apps/printers/printers.elf +LIBS := $(LIBDIR)/tls/libtls.a $(LIBDIR)/bearssl/libbearssl.a $(LIBDIR)/libc/liblibc.a + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): $(OBJS) $(LINK_LD) Makefile $(LIBS) + mkdir -p $(BINDIR)/apps/printers + $(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@ + +$(OBJDIR)/%.o: %.cpp Makefile + mkdir -p $(OBJDIR) + $(CXX) $(CXXFLAGS) -c $< -o $@ + +-include $(OBJS:.o=.d) + +clean: + rm -rf $(OBJDIR) $(TARGET) diff --git a/programs/src/printers/font_data.cpp b/programs/src/printers/font_data.cpp new file mode 100644 index 0000000..a591f69 --- /dev/null +++ b/programs/src/printers/font_data.cpp @@ -0,0 +1 @@ +#include "../desktop/font_data.cpp" diff --git a/programs/src/printers/main.cpp b/programs/src/printers/main.cpp new file mode 100644 index 0000000..b451e1e --- /dev/null +++ b/programs/src/printers/main.cpp @@ -0,0 +1,789 @@ +/* + * main.cpp + * MontaukOS Printers - standalone Window Server app + * Copyright (c) 2026 Daniel Hammer + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +#include +#include +#include +} + +using namespace gui; +using namespace print; + +static constexpr int INIT_W = 560; +static constexpr int INIT_H = 500; +static constexpr int PAD_X = 16; +static constexpr int PAD_Y = 20; +static constexpr int BTN_H = 30; +static constexpr int ACTION_RADIUS = 4; +static constexpr int FIELD_H = 32; + +struct AppState { + char printer_uri[MAX_PATH_LEN]; + bool printers_loaded; + bool daemon_running; + int daemon_pid; + int queue_count; + int active_count; + int done_count; + int failed_count; + uint64_t last_refresh_ms; + + JobMeta last_active; + JobMeta last_failed; + bool has_active; + bool has_failed; + + IppCapabilities probe_caps; + bool probe_valid; + bool probe_ok; + char probe_message[128]; + char probe_detail[MAX_DEBUG_LEN]; + + char status_msg[128]; + uint64_t status_time; + + bool config_open; + char edit_uri[MAX_PATH_LEN]; + int edit_len; + char edit_error[128]; + + Color accent; +}; + +struct Layout { + Rect configure_btn; + Rect clear_btn; + Rect probe_btn; + Rect start_btn; + Rect refresh_btn; + Rect test_btn; + + Rect dialog; + Rect dialog_input; + Rect dialog_save; + Rect dialog_cancel; +}; + +static WsWindow g_win; +static AppState g_app = {}; + +static void str_append(char* dst, const char* src, int max) { + int len = montauk::slen(dst); + int i = 0; + while (src[i] && len < max - 1) dst[len++] = src[i++]; + dst[len] = '\0'; +} + +static bool status_visible() { + return g_app.status_msg[0] && + (montauk::get_milliseconds() - g_app.status_time < 4000); +} + +static void set_status(const char* msg) { + montauk::strncpy(g_app.status_msg, msg ? msg : "", (int)sizeof(g_app.status_msg) - 1); + g_app.status_time = montauk::get_milliseconds(); +} + +static void clear_probe() { + montauk::memset(&g_app.probe_caps, 0, sizeof(g_app.probe_caps)); + g_app.probe_valid = false; + g_app.probe_ok = false; + g_app.probe_message[0] = '\0'; + g_app.probe_detail[0] = '\0'; +} + +static void set_probe_transport_detail(const IppUri& uri, const char* resolution) { + snprintf(g_app.probe_detail, sizeof(g_app.probe_detail), + "host=%s ip=%s port=%u path=%s %s", + uri.host, + resolution && *resolution ? resolution : "unresolved", + (unsigned)uri.port, + uri.path[0] ? uri.path : "/", + uri.use_tls ? "tls" : "plain"); +} + +static void set_accent(Color accent) { + g_app.accent = accent; +} + +static void load_accent() { + set_accent(colors::ACCENT); + + char user[64]; + montauk::memset(user, 0, sizeof(user)); + if (montauk::getuser(user, sizeof(user)) > 0 && user[0]) { + auto doc = montauk::config::load_user(user, "desktop"); + int64_t accent = doc.get_int("appearance.accent_color", -1); + if (accent >= 0) { + set_accent(Color::from_rgb( + (uint8_t)((accent >> 16) & 0xFF), + (uint8_t)((accent >> 8) & 0xFF), + (uint8_t)(accent & 0xFF))); + doc.destroy(); + return; + } + doc.destroy(); + } + + auto doc = montauk::config::load("desktop"); + int64_t accent = doc.get_int("appearance.accent_color", -1); + if (accent >= 0) { + set_accent(Color::from_rgb( + (uint8_t)((accent >> 16) & 0xFF), + (uint8_t)((accent >> 8) & 0xFF), + (uint8_t)(accent & 0xFF))); + } + doc.destroy(); +} + +static int count_dir_entries(const char* path) { + DIR* d = opendir(path); + if (!d) return 0; + + int count = 0; + struct dirent* ent = nullptr; + while ((ent = readdir(d)) != nullptr) { + if (ent->d_name[0] == '.') continue; + count++; + } + closedir(d); + return count; +} + +static bool load_latest_job(const char* dir, JobMeta* out) { + if (out) zero_job(out); + DIR* d = opendir(dir); + if (!d) return false; + + bool found = false; + char best_key[32] = {}; + struct dirent* ent = nullptr; + while ((ent = readdir(d)) != nullptr) { + if (ent->d_name[0] == '.') continue; + + char path[MAX_PATH_LEN]; + path_join(path, sizeof(path), dir, ent->d_name); + + JobMeta job = {}; + if (!load_job_from_path(path, &job)) continue; + + const char* key = job.updated_at[0] ? job.updated_at : job.created_at; + if (!found || strcmp(key, best_key) > 0) { + found = true; + safe_copy(best_key, sizeof(best_key), key); + if (out) *out = job; + } + } + + closedir(d); + return found; +} + +static void text_fit(const char* src, char* out, int out_len, int max_w) { + if (!src || !out || out_len <= 0) return; + montauk::strncpy(out, src, out_len - 1); + if (text_width(out) <= max_w) return; + + const char* ell = "..."; + int keep = montauk::slen(src); + while (keep > 0) { + char tmp[MAX_DEBUG_LEN]; + int n = keep < (int)sizeof(tmp) - 4 ? keep : (int)sizeof(tmp) - 4; + for (int i = 0; i < n; i++) tmp[i] = src[i]; + tmp[n] = '\0'; + str_append(tmp, ell, (int)sizeof(tmp)); + if (text_width(tmp) <= max_w) { + montauk::strncpy(out, tmp, out_len - 1); + return; + } + keep--; + } + montauk::strncpy(out, ell, out_len - 1); +} + +static void draw_action_btn(Canvas& c, const Rect& r, const char* label, + bool enabled, bool primary) { + Color bg; + Color fg; + if (!enabled) { + bg = Color::from_rgb(0xE6, 0xE6, 0xE6); + fg = Color::from_rgb(0x9A, 0x9A, 0x9A); + } else if (primary) { + bg = g_app.accent; + fg = colors::WHITE; + } else { + bg = colors::WINDOW_BG; + fg = colors::TEXT_COLOR; + } + + c.fill_rounded_rect(r.x, r.y, r.w, r.h, ACTION_RADIUS, bg); + if (enabled && !primary) c.rect(r.x, r.y, r.w, r.h, colors::BORDER); + if (!enabled) c.rect(r.x, r.y, r.w, r.h, Color::from_rgb(0xD4, 0xD4, 0xD4)); + + int tw = text_width(label); + int fh = system_font_height(); + c.text(r.x + (r.w - tw) / 2, r.y + (r.h - fh) / 2, label, fg); +} + +static void draw_input_field(Canvas& c, int x, int y, int w, int h, + const char* label, const char* value, bool focused) { + int sfh = system_font_height(); + Color dim = Color::from_rgb(0x88, 0x88, 0x88); + + c.text(x, y, label, dim); + y += sfh + 2; + c.fill_rounded_rect(x, y, w, h, 3, Color::from_rgb(0xF5, 0xF5, 0xF5)); + c.rect(x, y, w, h, focused ? g_app.accent : colors::BORDER); + c.text(x + 8, y + (h - sfh) / 2, value, colors::TEXT_COLOR); + + if (focused) { + int tw = text_width(value); + c.fill_rect(x + 8 + tw, y + 6, 2, h - 12, g_app.accent); + } +} + +static void compute_layout(Layout* lo) { + int sfh = system_font_height(); + int x = PAD_X; + int y = PAD_Y; + + if (status_visible()) y += sfh + 12; + + lo->configure_btn = {g_win.width - x - 112 - 8 - 84, y, 112, BTN_H}; + lo->clear_btn = {lo->configure_btn.x + 120, y, 84, BTN_H}; + + y += BTN_H + 8; + y += sfh + 4; + y += sfh + 14; + y += 16; + + lo->probe_btn = {g_win.width - x - 92, y, 92, BTN_H}; + + y += BTN_H + 8; + y += sfh + 4; + y += sfh + 4; + y += sfh + 4; + if (g_app.probe_valid && g_app.probe_ok && g_app.probe_detail[0]) y += sfh + 4; + y += sfh + 14; + y += 16; + + lo->start_btn = {g_win.width - x - 118 - 8 - 86, y, 118, BTN_H}; + lo->refresh_btn = {lo->start_btn.x + 126, y, 86, BTN_H}; + + y += BTN_H + 8; + y += sfh + 6; + y += sfh + 4; + y += sfh + 4; + if (g_app.has_active) y += sfh + 4; + if (g_app.has_active && g_app.last_active.debug_info[0]) y += sfh + 4; + if (g_app.has_failed) { + y += sfh + 4; + if (g_app.last_failed.debug_info[0]) y += sfh + 4; + } + y += 14; + y += 16; + + lo->test_btn = {g_win.width - x - 132, y, 132, BTN_H}; + + int dw = 408; + int dh = 220; + lo->dialog = {(g_win.width - dw) / 2, (g_win.height - dh) / 2, dw, dh}; + lo->dialog_input = {lo->dialog.x + 16, lo->dialog.y + 58, dw - 32, FIELD_H}; + lo->dialog_save = {lo->dialog.x + 16, lo->dialog.y + dh - BTN_H - 16, 80, BTN_H}; + lo->dialog_cancel = {lo->dialog.x + 104, lo->dialog.y + dh - BTN_H - 16, 80, BTN_H}; +} + +static void refresh_state(bool force = false) { + uint64_t now = montauk::get_milliseconds(); + if (!force && g_app.printers_loaded && now - g_app.last_refresh_ms < 1000) return; + + char previous_uri[MAX_PATH_LEN]; + safe_copy(previous_uri, sizeof(previous_uri), g_app.printer_uri); + + ensure_spool_dirs(); + + if (!read_default_printer_uri(g_app.printer_uri, sizeof(g_app.printer_uri))) + g_app.printer_uri[0] = '\0'; + + if (!montauk::streq(previous_uri, g_app.printer_uri)) + clear_probe(); + + g_app.daemon_pid = -1; + g_app.daemon_running = daemon_is_running(&g_app.daemon_pid); + g_app.queue_count = count_dir_entries(SPOOL_QUEUE_DIR); + g_app.active_count = count_dir_entries(SPOOL_ACTIVE_DIR); + g_app.done_count = count_dir_entries(SPOOL_DONE_DIR); + g_app.failed_count = count_dir_entries(SPOOL_FAILED_DIR); + g_app.has_active = load_latest_job(SPOOL_ACTIVE_DIR, &g_app.last_active); + g_app.has_failed = load_latest_job(SPOOL_FAILED_DIR, &g_app.last_failed); + g_app.printers_loaded = true; + g_app.last_refresh_ms = now; +} + +static void probe_printer() { + clear_probe(); + + if (!g_app.printer_uri[0]) { + safe_copy(g_app.probe_message, sizeof(g_app.probe_message), "No printer configured"); + g_app.probe_valid = true; + g_app.probe_ok = false; + set_status("No printer configured"); + return; + } + + char err[128] = {}; + IppUri normalized = {}; + if (!normalize_ipp_uri(g_app.printer_uri, &normalized, err, sizeof(err))) { + safe_copy(g_app.probe_message, sizeof(g_app.probe_message), err); + g_app.probe_valid = true; + g_app.probe_ok = false; + set_status("Printer probe failed"); + return; + } + + uint32_t ip = 0; + if (!resolve_host(normalized.host, &ip)) { + set_probe_transport_detail(normalized, "unresolved"); + if (host_looks_like_mdns(normalized.host)) + safe_copy(g_app.probe_message, sizeof(g_app.probe_message), + ".local/mDNS hostnames are not supported yet"); + else + snprintf(g_app.probe_message, sizeof(g_app.probe_message), + "Could not resolve %s", normalized.host); + g_app.probe_valid = true; + g_app.probe_ok = false; + set_status("Printer probe failed"); + return; + } + + char ip_text[20]; + format_ipv4(ip_text, sizeof(ip_text), ip); + set_probe_transport_detail(normalized, ip_text); + + if (!ipp_get_printer_capabilities(normalized.normalized, &g_app.probe_caps, err, sizeof(err))) { + safe_copy(g_app.probe_message, sizeof(g_app.probe_message), err[0] ? err : "IPP probe failed"); + summarize_ipp_capabilities(&g_app.probe_caps, g_app.probe_detail, sizeof(g_app.probe_detail)); + g_app.probe_valid = true; + g_app.probe_ok = false; + set_status("Printer probe failed"); + return; + } + + safe_copy(g_app.probe_message, sizeof(g_app.probe_message), "Printer probe succeeded"); + summarize_ipp_capabilities(&g_app.probe_caps, g_app.probe_detail, sizeof(g_app.probe_detail)); + g_app.probe_valid = true; + g_app.probe_ok = true; + set_status("Printer probe succeeded"); +} + +static void save_configured_printer() { + if (g_app.edit_len == 0) { + safe_copy(g_app.edit_error, sizeof(g_app.edit_error), "Printer URI is required"); + return; + } + + char err[128] = {}; + IppUri uri = {}; + if (!normalize_ipp_uri(g_app.edit_uri, &uri, err, sizeof(err))) { + safe_copy(g_app.edit_error, sizeof(g_app.edit_error), err); + return; + } + + if (!write_default_printer_uri(uri.normalized)) { + safe_copy(g_app.edit_error, sizeof(g_app.edit_error), "Failed to save printer"); + return; + } + + g_app.config_open = false; + refresh_state(true); + set_status("Printer saved"); +} + +static void draw_modal(Canvas& c, const Layout& lo) { + c.fill_rounded_rect(lo.dialog.x, lo.dialog.y, lo.dialog.w, lo.dialog.h, 6, colors::WINDOW_BG); + c.rect(lo.dialog.x, lo.dialog.y, lo.dialog.w, lo.dialog.h, colors::BORDER); + + int sfh = system_font_height(); + int y = lo.dialog.y + 16; + c.text(lo.dialog.x + 16, y, "Default Printer", colors::TEXT_COLOR); + y += sfh + 6; + c.text(lo.dialog.x + 16, y, "Enter an IPP or IPPS destination URI.", Color::from_rgb(0x88, 0x88, 0x88)); + y += sfh + 10; + + draw_input_field(c, lo.dialog_input.x, lo.dialog_input.y - (sfh + 2), + lo.dialog_input.w, lo.dialog_input.h, "Printer URI", + g_app.edit_uri, true); + + y = lo.dialog_input.y + lo.dialog_input.h + 12; + c.text(lo.dialog.x + 16, y, "Example: ipp://printer.local/ipp/print", Color::from_rgb(0x88, 0x88, 0x88)); + y += sfh + 10; + + if (g_app.edit_error[0]) + c.text(lo.dialog.x + 16, y, g_app.edit_error, Color::from_rgb(0xD0, 0x3E, 0x3E)); + + draw_action_btn(c, lo.dialog_save, "Save", true, true); + draw_action_btn(c, lo.dialog_cancel, "Cancel", true, false); +} + +static void render() { + refresh_state(); + + Canvas c = g_win.canvas(); + c.fill(colors::WINDOW_BG); + + Color dim = Color::from_rgb(0x88, 0x88, 0x88); + Color success = Color::from_rgb(0x22, 0x88, 0x22); + Color danger = Color::from_rgb(0xCC, 0x33, 0x33); + int sfh = system_font_height(); + int x = PAD_X; + int y = PAD_Y; + int w = g_win.width - 2 * x; + Layout lo = {}; + compute_layout(&lo); + + if (status_visible()) { + c.text(x, y, g_app.status_msg, g_app.accent); + y += sfh + 12; + } + + // Default printer + c.text(x, y + 6, "Default Printer", colors::TEXT_COLOR); + draw_action_btn(c, lo.configure_btn, "Configure", true, true); + draw_action_btn(c, lo.clear_btn, "Clear", g_app.printer_uri[0] != '\0', false); + y += BTN_H + 8; + + char line[MAX_DEBUG_LEN]; + if (g_app.printer_uri[0]) text_fit(g_app.printer_uri, line, sizeof(line), w); + else safe_copy(line, sizeof(line), "No printer configured"); + c.text(x, y, line, g_app.printer_uri[0] ? colors::TEXT_COLOR : dim); + y += sfh + 4; + c.text(x, y, "System-wide IPP / IPPS destination", dim); + y += sfh + 14; + c.hline(x, y, w, colors::BORDER); + y += 16; + + // Connection + c.text(x, y + 6, "Connection", colors::TEXT_COLOR); + draw_action_btn(c, lo.probe_btn, "Probe", g_app.printer_uri[0] != '\0', false); + y += BTN_H + 8; + + if (g_app.printer_uri[0]) { + IppUri normalized = {}; + char err[128] = {}; + if (normalize_ipp_uri(g_app.printer_uri, &normalized, err, sizeof(err))) { + snprintf(line, sizeof(line), "Host: %s", normalized.host); + c.text(x, y, line, colors::TEXT_COLOR); + y += sfh + 4; + + uint32_t ip = 0; + if (resolve_host(normalized.host, &ip)) { + char ip_text[20]; + format_ipv4(ip_text, sizeof(ip_text), ip); + snprintf(line, sizeof(line), "Resolved: %s", ip_text); + } else if (host_looks_like_mdns(normalized.host)) { + safe_copy(line, sizeof(line), "Resolved: unavailable (.local/mDNS not supported yet)"); + } else { + safe_copy(line, sizeof(line), "Resolved: unavailable"); + } + text_fit(line, line, sizeof(line), w); + c.text(x, y, line, dim); + } else { + safe_copy(line, sizeof(line), "Host: invalid printer URI"); + c.text(x, y, line, danger); + y += sfh + 4; + text_fit(err, line, sizeof(line), w); + c.text(x, y, line, dim); + } + } else { + c.text(x, y, "Host: not configured", dim); + y += sfh + 4; + c.text(x, y, "Resolved: unavailable", dim); + } + y += sfh + 4; + + if (g_app.probe_valid) { + if (g_app.probe_ok && g_app.probe_caps.printer_name[0]) + snprintf(line, sizeof(line), "Printer: %s", g_app.probe_caps.printer_name); + else + snprintf(line, sizeof(line), "Probe: %s", + g_app.probe_message[0] ? g_app.probe_message : (g_app.probe_ok ? "ok" : "failed")); + text_fit(line, line, sizeof(line), w); + c.text(x, y, line, g_app.probe_ok ? colors::TEXT_COLOR : danger); + y += sfh + 4; + + if (g_app.probe_ok) { + snprintf(line, sizeof(line), "Caps: PDF %s, JPEG %s, text %s", + g_app.probe_caps.supports_pdf ? "yes" : "no", + g_app.probe_caps.supports_jpeg ? "yes" : "no", + g_app.probe_caps.supports_text ? "yes" : "no"); + } else if (g_app.probe_detail[0]) { + safe_copy(line, sizeof(line), g_app.probe_detail); + } else { + safe_copy(line, sizeof(line), "Run Probe to query printer capabilities."); + } + } else { + safe_copy(line, sizeof(line), "Probe: not run"); + c.text(x, y, line, dim); + y += sfh + 4; + safe_copy(line, sizeof(line), "Run Probe to query printer capabilities."); + } + text_fit(line, line, sizeof(line), w); + c.text(x, y, line, dim); + if (g_app.probe_valid && g_app.probe_ok && g_app.probe_detail[0]) { + y += sfh + 4; + text_fit(g_app.probe_detail, line, sizeof(line), w); + c.text(x, y, line, dim); + } + y += sfh + 14; + c.hline(x, y, w, colors::BORDER); + y += 16; + + // Spooler + c.text(x, y + 6, "Spooler", colors::TEXT_COLOR); + draw_action_btn(c, lo.start_btn, "Start Daemon", !g_app.daemon_running, false); + draw_action_btn(c, lo.refresh_btn, "Refresh", true, false); + + const char* daemon_label = g_app.daemon_running ? "Running" : "Stopped"; + int chip_w = text_width(daemon_label) + 18; + int chip_x = lo.start_btn.x - chip_w - 12; + if (chip_x < x + 70) chip_x = x + 70; + c.fill_rounded_rect(chip_x, y + 2, chip_w, sfh + 8, (sfh + 8) / 2, + g_app.daemon_running ? success : danger); + c.text(chip_x + 9, y + 6, daemon_label, colors::WHITE); + y += BTN_H + 8; + + if (g_app.daemon_running) + snprintf(line, sizeof(line), "Daemon PID %d", g_app.daemon_pid); + else + safe_copy(line, sizeof(line), "Spooler is not active"); + c.text(x, y, line, dim); + y += sfh + 6; + + snprintf(line, sizeof(line), "Queued: %d Printing: %d", g_app.queue_count, g_app.active_count); + c.text(x, y, line, colors::TEXT_COLOR); + y += sfh + 4; + snprintf(line, sizeof(line), "Completed: %d Failed: %d", g_app.done_count, g_app.failed_count); + c.text(x, y, line, colors::TEXT_COLOR); + y += sfh + 4; + + if (g_app.has_active) { + snprintf(line, sizeof(line), "Active: %s", g_app.last_active.job_name); + text_fit(line, line, sizeof(line), w); + c.text(x, y, line, dim); + y += sfh + 4; + + if (g_app.last_active.debug_info[0]) { + snprintf(line, sizeof(line), "Debug: %s", g_app.last_active.debug_info); + text_fit(line, line, sizeof(line), w); + c.text(x, y, line, dim); + y += sfh + 4; + } + } + + if (g_app.has_failed) { + snprintf(line, sizeof(line), "Last failure: %s", g_app.last_failed.status_message); + text_fit(line, line, sizeof(line), w); + c.text(x, y, line, danger); + y += sfh + 4; + + if (g_app.last_failed.debug_info[0]) { + snprintf(line, sizeof(line), "Debug: %s", g_app.last_failed.debug_info); + text_fit(line, line, sizeof(line), w); + c.text(x, y, line, dim); + y += sfh + 4; + } + } + + y += 10; + c.hline(x, y, w, colors::BORDER); + y += 16; + + // Test page + c.text(x, y + 6, "Test Page", colors::TEXT_COLOR); + draw_action_btn(c, lo.test_btn, "Print Test Page", g_app.printer_uri[0] != '\0', true); + y += BTN_H + 8; + c.text(x, y, "Queue a simple test page through the print spooler.", dim); + y += sfh + 4; + c.text(x, y, + g_app.printer_uri[0] ? "Uses the configured default printer." : "Configure a printer first.", + g_app.printer_uri[0] ? colors::TEXT_COLOR : dim); + + if (g_app.config_open) + draw_modal(c, lo); + + g_win.present(); +} + +static void handle_click(int mx, int my) { + Layout lo = {}; + compute_layout(&lo); + + if (g_app.config_open) { + if (lo.dialog_save.contains(mx, my)) { + save_configured_printer(); + } else if (lo.dialog_cancel.contains(mx, my)) { + g_app.config_open = false; + } + return; + } + + if (lo.configure_btn.contains(mx, my)) { + safe_copy(g_app.edit_uri, sizeof(g_app.edit_uri), g_app.printer_uri); + g_app.edit_len = montauk::slen(g_app.edit_uri); + g_app.edit_error[0] = '\0'; + g_app.config_open = true; + return; + } + + if (g_app.printer_uri[0] && lo.clear_btn.contains(mx, my)) { + if (montauk::fdelete(DEFAULT_PRINTER_PATH) < 0) set_status("Failed to clear printer"); + else { + refresh_state(true); + set_status("Printer cleared"); + } + return; + } + + if (g_app.printer_uri[0] && lo.probe_btn.contains(mx, my)) { + probe_printer(); + return; + } + + if (!g_app.daemon_running && lo.start_btn.contains(mx, my)) { + char err[128] = {}; + if (ensure_daemon_running(err, sizeof(err))) { + refresh_state(true); + set_status("Print spooler started"); + } else { + set_status(err[0] ? err : "Failed to start print spooler"); + } + return; + } + + if (lo.refresh_btn.contains(mx, my)) { + refresh_state(true); + set_status("Printer status refreshed"); + return; + } + + if (g_app.printer_uri[0] && lo.test_btn.contains(mx, my)) { + char job_id[48] = {}; + char err[128] = {}; + if (submit_test_page_job(nullptr, job_id, sizeof(job_id), err, sizeof(err))) { + refresh_state(true); + set_status("Test page queued"); + } else { + set_status(err[0] ? err : "Failed to queue test page"); + } + return; + } +} + +static void handle_key(const Montauk::KeyEvent& key) { + if (!key.pressed) return; + + if (g_app.config_open) { + if (key.scancode == 0x01) { + g_app.config_open = false; + return; + } + if (key.ascii == '\n' || key.ascii == '\r' || key.scancode == 0x1C) { + save_configured_printer(); + return; + } + if (key.ascii == '\b' || key.scancode == 0x0E) { + if (g_app.edit_len > 0) { + g_app.edit_len--; + g_app.edit_uri[g_app.edit_len] = '\0'; + } + return; + } + if (key.ascii >= 0x20 && key.ascii < 0x7F && g_app.edit_len < MAX_PATH_LEN - 1) { + g_app.edit_uri[g_app.edit_len++] = key.ascii; + g_app.edit_uri[g_app.edit_len] = '\0'; + } + return; + } + + if (key.scancode == 0x01) { + g_win.closed = true; + return; + } + + if (key.ascii == 'r' || key.ascii == 'R') { + refresh_state(true); + set_status("Printer status refreshed"); + } else if (key.ascii == 'p' || key.ascii == 'P') { + if (g_app.printer_uri[0]) probe_printer(); + } else if (key.ascii == 't' || key.ascii == 'T') { + if (g_app.printer_uri[0]) { + char job_id[48] = {}; + char err[128] = {}; + if (submit_test_page_job(nullptr, job_id, sizeof(job_id), err, sizeof(err))) { + refresh_state(true); + set_status("Test page queued"); + } else { + set_status(err[0] ? err : "Failed to queue test page"); + } + } + } +} + +extern "C" void _start() { + if (!fonts::init()) + montauk::exit(1); + + load_accent(); + refresh_state(true); + + if (!g_win.create("Printers", INIT_W, INIT_H)) + montauk::exit(1); + + render(); + + while (g_win.id >= 0 && !g_win.closed) { + Montauk::WinEvent ev; + int r = g_win.poll(&ev); + if (r < 0) break; + if (r == 0) { + montauk::sleep_ms(16); + continue; + } + + bool redraw = false; + + if (ev.type == 3) break; + if (ev.type == 2 || ev.type == 4) redraw = true; + + if (ev.type == 0) { + handle_key(ev.key); + redraw = true; + } else if (ev.type == 1) { + bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1); + if (clicked) { + handle_click(ev.mouse.x, ev.mouse.y); + redraw = true; + } + } + + if (redraw) + render(); + } + + g_win.destroy(); + montauk::exit(0); +} diff --git a/programs/src/printers/manifest.toml b/programs/src/printers/manifest.toml new file mode 100644 index 0000000..f5e54c7 --- /dev/null +++ b/programs/src/printers/manifest.toml @@ -0,0 +1,8 @@ +[app] +name = "Printers" +binary = "printers.elf" +icon = "preferences-devices-printer.svg" + +[menu] +category = "System" +visible = true diff --git a/programs/src/printers/stb_truetype_impl.cpp b/programs/src/printers/stb_truetype_impl.cpp new file mode 100644 index 0000000..dd91785 --- /dev/null +++ b/programs/src/printers/stb_truetype_impl.cpp @@ -0,0 +1,33 @@ +/* + * stb_truetype_impl.cpp + * Single compilation unit for stb_truetype in MontaukOS freestanding environment + * Copyright (c) 2026 Daniel Hammer + */ + +#include +#include +#include +#include +#include + +#define STBTT_ifloor(x) ((int) stb_floor(x)) +#define STBTT_iceil(x) ((int) stb_ceil(x)) +#define STBTT_sqrt(x) stb_sqrt(x) +#define STBTT_pow(x,y) stb_pow(x,y) +#define STBTT_fmod(x,y) stb_fmod(x,y) +#define STBTT_cos(x) stb_cos(x) +#define STBTT_acos(x) stb_acos(x) +#define STBTT_fabs(x) stb_fabs(x) + +#define STBTT_malloc(x,u) ((void)(u), montauk::malloc(x)) +#define STBTT_free(x,u) ((void)(u), montauk::mfree(x)) + +#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n) +#define STBTT_memset(d,v,n) montauk::memset(d,v,n) + +#define STBTT_strlen(x) montauk::slen(x) + +#define STBTT_assert(x) ((void)(x)) + +#define STB_TRUETYPE_IMPLEMENTATION +#include diff --git a/scripts/install_apps.sh b/scripts/install_apps.sh index 57223b9..2f1486a 100755 --- a/scripts/install_apps.sh +++ b/scripts/install_apps.sh @@ -34,6 +34,7 @@ APPS=( "klog|apps/scalable/utilities-terminal.svg" "procmgr|apps/scalable/system-monitor.svg" "calculator|apps/scalable/accessories-calculator.svg" + "printers|devices/scalable/preferences-devices-printer.svg" "rpgdemo|apps/scalable/utilities-terminal.svg" "paint|apps/scalable/kolourpaint.svg" "screenshot|apps/scalable/gnome-screenshot.svg"