From 806f46eb6afee3043e886dcfe4933c1e6c38a100 Mon Sep 17 00:00:00 2001 From: Benjamin Nguyen Date: Thu, 24 Jul 2025 00:20:10 -0700 Subject: [PATCH 1/6] implement print queue and dev printer --- docker-compose.printer.dev.yml | 21 +++++++++++++++ printer/Dockerfile.printer.dev | 28 ++++++++++++++++++++ printer/print_queue.py | 34 ++++++++++++++++++++++++ printer/server.py | 47 ++++++++++++++++++++++++++-------- printer/what_dev.sh | 9 +++++++ 5 files changed, 129 insertions(+), 10 deletions(-) create mode 100644 docker-compose.printer.dev.yml create mode 100644 printer/Dockerfile.printer.dev create mode 100644 printer/print_queue.py create mode 100644 printer/what_dev.sh diff --git a/docker-compose.printer.dev.yml b/docker-compose.printer.dev.yml new file mode 100644 index 0000000..33bffc6 --- /dev/null +++ b/docker-compose.printer.dev.yml @@ -0,0 +1,21 @@ +version: '2' +services: + sce-printer: + container_name: sce-printer-dev + build: + context: . + dockerfile: ./printer/Dockerfile.printer.dev + ports: + # we use port 14000 as that is what the website expects + # the printing server to be running on + - 14000:14000 + volumes: + - ./config:/app/config + - ./printer:/app/printer + - ./tmp:/tmp + command: + - --development + - --port=14000 + - --dont-delete-pdfs + - --config-json-path=/app/config/config.json + - --dev-printer diff --git a/printer/Dockerfile.printer.dev b/printer/Dockerfile.printer.dev new file mode 100644 index 0000000..c6668ae --- /dev/null +++ b/printer/Dockerfile.printer.dev @@ -0,0 +1,28 @@ +# Base image from https://github.com/DrPsychick/docker-cups-airprint +# Docker images are here https://hub.docker.com/r/drpsychick/airprint-bridge/tags +FROM drpsychick/airprint-bridge:jammy + +WORKDIR /app +RUN apt-get update + +RUN apt install -y python3 python3-pip python3-venv jq ssh + +# Create the virtual environment with Python +RUN python3 -m venv /opt/venv + +# Set the virtual environment as the default Python environment +ENV PATH="/opt/venv/bin:$PATH" + +COPY ./printer/requirements.txt /app/printer/requirements.txt + +RUN /opt/venv/bin/pip install -r /app/printer/requirements.txt + +COPY ./config/config.json /app/config/config.json + +COPY ./printer/what_dev.sh /app/printer/what_dev.sh + +COPY ./printer/*.py /app/printer/ + +EXPOSE 9000 + +ENTRYPOINT ["./printer/what_dev.sh"] diff --git a/printer/print_queue.py b/printer/print_queue.py new file mode 100644 index 0000000..73bdef6 --- /dev/null +++ b/printer/print_queue.py @@ -0,0 +1,34 @@ +import subprocess +import time +import logging + +class PrintQueue: + _queue = [] + + def add(self, file_name): + self._queue.append(file_name) + + def in_queue(self, file_name): + return file_name in self._queue + + def feed_into_printer(self): + while True: + time.sleep(1) + + if self._queue.__len__() == 0: + continue + + proc = subprocess.Popen( + 'lpstat -o', + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + + printer_job_count = str(proc.stdout.read()).count("\n") + + if printer_job_count <= 2: + logging.info("FED REQUEST INTO PRINTER") + self._queue.pop(0) + \ No newline at end of file diff --git a/printer/server.py b/printer/server.py index 1783831..ff50260 100644 --- a/printer/server.py +++ b/printer/server.py @@ -7,6 +7,8 @@ import time import uuid import collector +import asyncio +from print_queue import PrintQueue from fastapi import FastAPI, File, Form, HTTPException, UploadFile from fastapi.middleware.cors import CORSMiddleware @@ -17,6 +19,7 @@ from metrics import MetricsHandler +print_queue = PrintQueue() metrics_handler = MetricsHandler.instance() app = FastAPI() @@ -35,7 +38,6 @@ logging.getLogger("uvicorn.access").setLevel(logging.WARNING) logging.getLogger("uvicorn.error").setLevel(logging.WARNING) - def get_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument( @@ -49,6 +51,12 @@ def get_args() -> argparse.Namespace: default=9000, help="PORT name for server to listen on. defaults to 9000", ) + parser.add_argument( + "--dev-printer", + action="store_true", + default=False, + help="specify if the development virtual printer should be used", + ) parser.add_argument( "--config-json-path", default="/app/config/config.json", @@ -110,10 +118,11 @@ def send_file_to_printer( maybe_page_range = f"-o page-ranges={page_range}" # only the right printer works right now, so we default to it - PRINTER_NAME = os.environ.get("RIGHT_PRINTER_NAME") - command = f"lp -n {num_copies} {maybe_page_range} -o sides={sides} -o media=na_letter_8.5x11in -d {PRINTER_NAME} {file_path}" + PRINTER_NAME = "dev_printer" if args.dev_printer else os.environ.get("RIGHT_PRINTER_NAME") + command = f"lp -H hold -n {num_copies} {maybe_page_range} -o sides={sides} -o media=na_letter_8.5x11in -d {PRINTER_NAME} {file_path}" metrics_handler.print_jobs_recieved.inc() - if args.development: + + if args.development and not args.dev_printer: logging.warning( f"server is in development mode, command would've been `{command}`" ) @@ -126,6 +135,7 @@ def send_file_to_printer( stderr=subprocess.PIPE, text=True, ) + print_job.wait() if print_job.returncode != 0: @@ -145,16 +155,14 @@ def send_file_to_printer( # with code 0 but the output could not be parsed for a job id. return '' - def maybe_delete_pdf(file_path): if args.dont_delete_pdfs: - logging.info( - f"--dont-delete-pdfs is set, skipping deletion of file {file_path}" - ) + #logging.info( + # f"--dont-delete-pdfs is set, skipping deletion of file {file_path}" + #) return pathlib.Path(file_path).unlink() - @app.get("/healthcheck/printer") def api(): metrics_handler.last_health_check_request.set(int(time.time())) @@ -178,6 +186,18 @@ async def read_item( "sides": string value from user input on clark frontend; we insert this into the lp command, } """ + + if args.dev_printer or not args.development: + print_queue.add(file.filename) + timeout = 0 + while print_queue.in_queue(file.filename): + timeout += 1 + + if timeout > 120: + raise Exception("/print TIMED OUT AFTER 120 SECONDS WHILE WAITING IN THE PRINTER QUEUE") + + await asyncio.sleep(1) + try: base = pathlib.Path("/tmp") file_id = str(uuid.uuid4()) @@ -201,7 +221,7 @@ async def read_item( status_code=500, detail="printing failed, check logs", ) - + # we have a separate __name__ check here due to how FastAPI starts # a server. the file is first ran (where __name__ == "__main__") @@ -212,6 +232,13 @@ async def read_item( # the thread interacts with an instance different than the one the # server uses if __name__ == "server": + if args.dev_printer or not args.development: + queue_thread = threading.Thread( + target=print_queue.feed_into_printer, + daemon=True + ) + queue_thread.start() + if not args.development: # set the last time we opened an ssh tunnel to now because # when the script runs for the first time, we did so in what.sh diff --git a/printer/what_dev.sh b/printer/what_dev.sh new file mode 100644 index 0000000..349ee3e --- /dev/null +++ b/printer/what_dev.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +/root/start-cups.sh > /dev/null 2>&1 & + +sleep 10 + +lpadmin -p dev_printer -E -v file:///dev/null + +python3 /app/printer/server.py $@ From bfc8848d9373fe5aae0d670283b6a8336ccf23c7 Mon Sep 17 00:00:00 2001 From: Benjamin Nguyen Date: Thu, 24 Jul 2025 00:22:53 -0700 Subject: [PATCH 2/6] fix misc --- printer/print_queue.py | 1 - printer/server.py | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/printer/print_queue.py b/printer/print_queue.py index 73bdef6..eb416ed 100644 --- a/printer/print_queue.py +++ b/printer/print_queue.py @@ -31,4 +31,3 @@ def feed_into_printer(self): if printer_job_count <= 2: logging.info("FED REQUEST INTO PRINTER") self._queue.pop(0) - \ No newline at end of file diff --git a/printer/server.py b/printer/server.py index ff50260..a2128ad 100644 --- a/printer/server.py +++ b/printer/server.py @@ -157,9 +157,9 @@ def send_file_to_printer( def maybe_delete_pdf(file_path): if args.dont_delete_pdfs: - #logging.info( - # f"--dont-delete-pdfs is set, skipping deletion of file {file_path}" - #) + logging.info( + f"--dont-delete-pdfs is set, skipping deletion of file {file_path}" + ) return pathlib.Path(file_path).unlink() From aa1ca48eefcf47b84babe162bb8de8a15afa7871 Mon Sep 17 00:00:00 2001 From: Benjamin Nguyen Date: Thu, 24 Jul 2025 00:40:41 -0700 Subject: [PATCH 3/6] remove debug hold on lp --- printer/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/printer/server.py b/printer/server.py index a2128ad..c97ed45 100644 --- a/printer/server.py +++ b/printer/server.py @@ -119,7 +119,7 @@ def send_file_to_printer( # only the right printer works right now, so we default to it PRINTER_NAME = "dev_printer" if args.dev_printer else os.environ.get("RIGHT_PRINTER_NAME") - command = f"lp -H hold -n {num_copies} {maybe_page_range} -o sides={sides} -o media=na_letter_8.5x11in -d {PRINTER_NAME} {file_path}" + command = f"lp -n {num_copies} {maybe_page_range} -o sides={sides} -o media=na_letter_8.5x11in -d {PRINTER_NAME} {file_path}" metrics_handler.print_jobs_recieved.inc() if args.development and not args.dev_printer: From 329aa12c8b7c0ddd76659c6c27c8c313a98f4601 Mon Sep 17 00:00:00 2001 From: Benjamin Nguyen Date: Fri, 25 Jul 2025 21:08:36 -0700 Subject: [PATCH 4/6] add asyncio lock to read_item; if actual queue has spots just send directly to printer --- printer/print_queue.py | 25 +++++++++------- printer/server.py | 65 +++++++++++++++++++++--------------------- 2 files changed, 47 insertions(+), 43 deletions(-) diff --git a/printer/print_queue.py b/printer/print_queue.py index eb416ed..be03119 100644 --- a/printer/print_queue.py +++ b/printer/print_queue.py @@ -11,6 +11,19 @@ def add(self, file_name): def in_queue(self, file_name): return file_name in self._queue + def actual_queue_available(self): + proc = subprocess.Popen( + 'lpstat -o', + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + + printer_job_count = str(proc.stdout.read()).count("\n") + + return printer_job_count <= 2 + def feed_into_printer(self): while True: time.sleep(1) @@ -18,16 +31,6 @@ def feed_into_printer(self): if self._queue.__len__() == 0: continue - proc = subprocess.Popen( - 'lpstat -o', - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - - printer_job_count = str(proc.stdout.read()).count("\n") - - if printer_job_count <= 2: + if self.actual_queue_available(): logging.info("FED REQUEST INTO PRINTER") self._queue.pop(0) diff --git a/printer/server.py b/printer/server.py index c97ed45..7a4af6a 100644 --- a/printer/server.py +++ b/printer/server.py @@ -20,6 +20,7 @@ print_queue = PrintQueue() +printer_lock = asyncio.Lock() metrics_handler = MetricsHandler.instance() app = FastAPI() @@ -119,7 +120,7 @@ def send_file_to_printer( # only the right printer works right now, so we default to it PRINTER_NAME = "dev_printer" if args.dev_printer else os.environ.get("RIGHT_PRINTER_NAME") - command = f"lp -n {num_copies} {maybe_page_range} -o sides={sides} -o media=na_letter_8.5x11in -d {PRINTER_NAME} {file_path}" + command = f"lp -H hold -n {num_copies} {maybe_page_range} -o sides={sides} -o media=na_letter_8.5x11in -d {PRINTER_NAME} {file_path}" metrics_handler.print_jobs_recieved.inc() if args.development and not args.dev_printer: @@ -186,41 +187,41 @@ async def read_item( "sides": string value from user input on clark frontend; we insert this into the lp command, } """ + async with printer_lock: + if (args.dev_printer or not args.development) and not print_queue.actual_queue_available() : + print_queue.add(file.filename) + timeout = 0 + while print_queue.in_queue(file.filename): + timeout += 1 - if args.dev_printer or not args.development: - print_queue.add(file.filename) - timeout = 0 - while print_queue.in_queue(file.filename): - timeout += 1 - - if timeout > 120: - raise Exception("/print TIMED OUT AFTER 120 SECONDS WHILE WAITING IN THE PRINTER QUEUE") - - await asyncio.sleep(1) + if timeout > 300: + raise Exception("/print TIMED OUT AFTER 300 SECONDS WHILE WAITING IN THE PRINTER QUEUE") + + await asyncio.sleep(1) - try: - base = pathlib.Path("/tmp") - file_id = str(uuid.uuid4()) - file_path = str(base / file_id) - with open(file_path, "wb") as f: - f.write(await file.read()) - print_id = send_file_to_printer( - str(file_path), - copies, - sides=sides, - ) + try: + base = pathlib.Path("/tmp") + file_id = str(uuid.uuid4()) + file_path = str(base / file_id) + with open(file_path, "wb") as f: + f.write(await file.read()) + print_id = send_file_to_printer( + str(file_path), + copies, + sides=sides, + ) - maybe_delete_pdf(file_path) + maybe_delete_pdf(file_path) - if not args.development and print_id is None: - raise Exception("unable to extract print id from print request") - return {"print_id": print_id} - except Exception: - logging.exception("printing failed!") - return HTTPException( - status_code=500, - detail="printing failed, check logs", - ) + if not args.development and print_id is None: + raise Exception("unable to extract print id from print request") + return {"print_id": print_id} + except Exception: + logging.exception("printing failed!") + return HTTPException( + status_code=500, + detail="printing failed, check logs", + ) # we have a separate __name__ check here due to how FastAPI starts From 77bb86c6d4afa8b9fa47584ccf299ed0f5481970 Mon Sep 17 00:00:00 2001 From: Benjamin Nguyen Date: Fri, 25 Jul 2025 21:09:33 -0700 Subject: [PATCH 5/6] remove debug hold --- printer/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/printer/server.py b/printer/server.py index 7a4af6a..ac2080d 100644 --- a/printer/server.py +++ b/printer/server.py @@ -120,7 +120,7 @@ def send_file_to_printer( # only the right printer works right now, so we default to it PRINTER_NAME = "dev_printer" if args.dev_printer else os.environ.get("RIGHT_PRINTER_NAME") - command = f"lp -H hold -n {num_copies} {maybe_page_range} -o sides={sides} -o media=na_letter_8.5x11in -d {PRINTER_NAME} {file_path}" + command = f"lp -n {num_copies} {maybe_page_range} -o sides={sides} -o media=na_letter_8.5x11in -d {PRINTER_NAME} {file_path}" metrics_handler.print_jobs_recieved.inc() if args.development and not args.dev_printer: From ca5d55a812554cf40e7a23c650897e2294994818 Mon Sep 17 00:00:00 2001 From: Benjamin Nguyen Date: Sun, 27 Jul 2025 22:57:46 -0700 Subject: [PATCH 6/6] remove dev virtual printer stuff --- docker-compose.printer.dev.yml | 21 --------------------- printer/Dockerfile.printer.dev | 28 ---------------------------- printer/server.py | 14 ++++---------- printer/what_dev.sh | 9 --------- 4 files changed, 4 insertions(+), 68 deletions(-) delete mode 100644 docker-compose.printer.dev.yml delete mode 100644 printer/Dockerfile.printer.dev delete mode 100644 printer/what_dev.sh diff --git a/docker-compose.printer.dev.yml b/docker-compose.printer.dev.yml deleted file mode 100644 index 33bffc6..0000000 --- a/docker-compose.printer.dev.yml +++ /dev/null @@ -1,21 +0,0 @@ -version: '2' -services: - sce-printer: - container_name: sce-printer-dev - build: - context: . - dockerfile: ./printer/Dockerfile.printer.dev - ports: - # we use port 14000 as that is what the website expects - # the printing server to be running on - - 14000:14000 - volumes: - - ./config:/app/config - - ./printer:/app/printer - - ./tmp:/tmp - command: - - --development - - --port=14000 - - --dont-delete-pdfs - - --config-json-path=/app/config/config.json - - --dev-printer diff --git a/printer/Dockerfile.printer.dev b/printer/Dockerfile.printer.dev deleted file mode 100644 index c6668ae..0000000 --- a/printer/Dockerfile.printer.dev +++ /dev/null @@ -1,28 +0,0 @@ -# Base image from https://github.com/DrPsychick/docker-cups-airprint -# Docker images are here https://hub.docker.com/r/drpsychick/airprint-bridge/tags -FROM drpsychick/airprint-bridge:jammy - -WORKDIR /app -RUN apt-get update - -RUN apt install -y python3 python3-pip python3-venv jq ssh - -# Create the virtual environment with Python -RUN python3 -m venv /opt/venv - -# Set the virtual environment as the default Python environment -ENV PATH="/opt/venv/bin:$PATH" - -COPY ./printer/requirements.txt /app/printer/requirements.txt - -RUN /opt/venv/bin/pip install -r /app/printer/requirements.txt - -COPY ./config/config.json /app/config/config.json - -COPY ./printer/what_dev.sh /app/printer/what_dev.sh - -COPY ./printer/*.py /app/printer/ - -EXPOSE 9000 - -ENTRYPOINT ["./printer/what_dev.sh"] diff --git a/printer/server.py b/printer/server.py index ac2080d..a0ebfe1 100644 --- a/printer/server.py +++ b/printer/server.py @@ -52,12 +52,6 @@ def get_args() -> argparse.Namespace: default=9000, help="PORT name for server to listen on. defaults to 9000", ) - parser.add_argument( - "--dev-printer", - action="store_true", - default=False, - help="specify if the development virtual printer should be used", - ) parser.add_argument( "--config-json-path", default="/app/config/config.json", @@ -119,11 +113,11 @@ def send_file_to_printer( maybe_page_range = f"-o page-ranges={page_range}" # only the right printer works right now, so we default to it - PRINTER_NAME = "dev_printer" if args.dev_printer else os.environ.get("RIGHT_PRINTER_NAME") + PRINTER_NAME = os.environ.get("RIGHT_PRINTER_NAME") command = f"lp -n {num_copies} {maybe_page_range} -o sides={sides} -o media=na_letter_8.5x11in -d {PRINTER_NAME} {file_path}" metrics_handler.print_jobs_recieved.inc() - if args.development and not args.dev_printer: + if args.development: logging.warning( f"server is in development mode, command would've been `{command}`" ) @@ -188,7 +182,7 @@ async def read_item( } """ async with printer_lock: - if (args.dev_printer or not args.development) and not print_queue.actual_queue_available() : + if not args.development and not print_queue.actual_queue_available(): print_queue.add(file.filename) timeout = 0 while print_queue.in_queue(file.filename): @@ -233,7 +227,7 @@ async def read_item( # the thread interacts with an instance different than the one the # server uses if __name__ == "server": - if args.dev_printer or not args.development: + if not args.development: queue_thread = threading.Thread( target=print_queue.feed_into_printer, daemon=True diff --git a/printer/what_dev.sh b/printer/what_dev.sh deleted file mode 100644 index 349ee3e..0000000 --- a/printer/what_dev.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh - -/root/start-cups.sh > /dev/null 2>&1 & - -sleep 10 - -lpadmin -p dev_printer -E -v file:///dev/null - -python3 /app/printer/server.py $@