feat: support for IPP printing + test page
This commit is contained in:
@@ -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)
|
||||
@@ -0,0 +1,360 @@
|
||||
/*
|
||||
* main.cpp
|
||||
* MontaukOS print control utility
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <print/print.hpp>
|
||||
|
||||
extern "C" {
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
}
|
||||
|
||||
using namespace print;
|
||||
|
||||
static void usage() {
|
||||
printf("Usage:\n");
|
||||
printf(" printctl set-printer <ipp://host[:port]/path>\n");
|
||||
printf(" printctl show-printer\n");
|
||||
printf(" printctl print <file> [--printer <uri>] [--name <job>] [--wait]\n");
|
||||
printf(" printctl test-page [--printer <uri>] [--wait] [--no-wait]\n");
|
||||
printf(" printctl status [--verbose]\n");
|
||||
printf(" printctl inspect <job-id>\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);
|
||||
}
|
||||
Reference in New Issue
Block a user