From 63a791c02be2b6da7cc0e4a4228ab9f2f5c51094 Mon Sep 17 00:00:00 2001 From: deeplow Date: Fri, 13 Dec 2024 21:28:03 +0000 Subject: [PATCH 1/5] Initial support for IPP / driverless printing Changes export logic (in sd-devices) to be able to detect IPP printers without breaking compatibility with the old system. To achive this, ipp-usb is used in combination with Avahi. The former detects IPP-compatible printers and creates a local IPP server. Avahi allows for the discovery of these printing servers such that print dialogs can display print information. Contrary to the legacy printer support, for driverless printing, no print queue is setup with `lpadmin`. Printers are automatically discovered. --- debian/control | 3 +- debian/securedrop-export.install | 1 + export/files/60-securedrop-export.preset | 4 ++ export/securedrop_export/print/service.py | 75 ++++++++++++++++------- 4 files changed, 61 insertions(+), 22 deletions(-) create mode 100644 export/files/60-securedrop-export.preset diff --git a/debian/control b/debian/control index 3a5f69fc8..03ecfff7c 100644 --- a/debian/control +++ b/debian/control @@ -14,7 +14,8 @@ Description: securedrop client for qubes workstation Package: securedrop-export Architecture: all -Depends: ${misc:Depends}, python3, udisks2, cups, printer-driver-brlaser, printer-driver-hpcups, system-config-printer, xpp, libcups2, gnome-disk-utility, libreoffice, +Depends: ${misc:Depends}, python3, udisks2, cups, cups-ipp-utils, printer-driver-brlaser, printer-driver-hpcups, + avahi-daemon, system-config-printer, xpp, libcups2, gnome-disk-utility, libreoffice, desktop-file-utils, shared-mime-info, libfile-mimeinfo-perl Description: Submission export scripts for SecureDrop Workstation This package provides scripts used by the SecureDrop Qubes Workstation to diff --git a/debian/securedrop-export.install b/debian/securedrop-export.install index b095f316f..1c2df926e 100644 --- a/debian/securedrop-export.install +++ b/debian/securedrop-export.install @@ -2,3 +2,4 @@ export/files/application-x-sd-export.xml usr/share/mime/packages export/files/send-to-usb.desktop usr/share/applications export/files/sd-logo.png usr/share/securedrop/icons export/files/tcrypt.conf etc/udisks2 +export/files/60-securedrop-export.preset usr/lib/systemd/system-preset diff --git a/export/files/60-securedrop-export.preset b/export/files/60-securedrop-export.preset new file mode 100644 index 000000000..7535e5b04 --- /dev/null +++ b/export/files/60-securedrop-export.preset @@ -0,0 +1,4 @@ +# avahi-daemon so that driverless printers are detected when +# plugged in via USB, overriding 75-qubes-vm.preset + +enable avahi-daemon.service \ No newline at end of file diff --git a/export/securedrop_export/print/service.py b/export/securedrop_export/print/service.py index 830820349..7db747a75 100644 --- a/export/securedrop_export/print/service.py +++ b/export/securedrop_export/print/service.py @@ -1,12 +1,10 @@ import logging import os -import signal import subprocess -import time from pathlib import Path from securedrop_export.directory import safe_mkdir -from securedrop_export.exceptions import ExportException, TimeoutException, handler +from securedrop_export.exceptions import ExportException from .status import Status @@ -143,32 +141,63 @@ def _check_printer_setup(self) -> None: Check printer setup. Raise ExportException if supported setup is not found. """ + legacy_printers = False + logger.info("Searching for printer") + + printers = self._get_printers_ipp() + if not printers: + # look for legacy printers after no IPP ones are detected + printers = self._get_printers_legacy() + legacy_printers = True + + if not printers: + logger.info("No supported printers connected") + raise ExportException(sdstatus=Status.ERROR_PRINTER_NOT_FOUND) + + if len(printers) > 1: + logger.info("Too many printers connected") + raise ExportException(sdstatus=Status.ERROR_MULTIPLE_PRINTERS_FOUND) + + printer_uri = printers[0] + if legacy_printers: # IPP printers are auto-detected by the print dialog + self._setup_printer(printer_uri) + + def _get_printers_legacy(self) -> list[str]: + logger.info("Searching for legacy printers") try: - logger.info("Searching for printer") output = subprocess.check_output(["sudo", "lpinfo", "-v"]) - printers = [x for x in output.decode("utf-8").split() if "usb://" in x] - if not printers: - logger.info("No usb printers connected") - raise ExportException(sdstatus=Status.ERROR_PRINTER_NOT_FOUND) + except subprocess.CalledProcessError as e: + logger.error(e) + raise ExportException(sdstatus=Status.ERROR_UNKNOWN) - supported_printers = [ - p for p in printers if any(sub in p for sub in self.SUPPORTED_PRINTERS) - ] - if not supported_printers: - logger.info(f"{printers} are unsupported printers") - raise ExportException(sdstatus=Status.ERROR_PRINTER_NOT_SUPPORTED) + discovered_printers = [x for x in output.decode("utf-8").split() if "usb://" in x] - if len(supported_printers) > 1: - logger.info("Too many usb printers connected") - raise ExportException(sdstatus=Status.ERROR_MULTIPLE_PRINTERS_FOUND) + supported_printers = [ + p for p in discovered_printers if any(sub in p for sub in self.SUPPORTED_PRINTERS) + ] + if not supported_printers: + logger.info(f"{discovered_printers} are unsupported printers") + raise ExportException(sdstatus=Status.ERROR_PRINTER_NOT_SUPPORTED) + + return supported_printers - printer_uri = printers[0] - printer_ppd = self._install_printer_ppd(printer_uri) - self._setup_printer(printer_uri, printer_ppd) + def _get_printers_ipp(self) -> list[str]: + logger.info("Searching for IPP printers (driverless)") + try: + discovered_printers = subprocess.check_output( + ["ippfind"], universal_newlines=True + ).split() except subprocess.CalledProcessError as e: logger.error(e) raise ExportException(sdstatus=Status.ERROR_UNKNOWN) + if discovered_printers: + logger.debug(f"Found IPP printers: {', '.join(discovered_printers)}") + else: + logger.debug("No IPP were found") + + return discovered_printers + def _get_printer_uri(self) -> str: """ Get the URI via lpinfo. Only accept URIs of supported printers. @@ -202,6 +231,9 @@ def _get_printer_uri(self) -> str: return printer_uri def _install_printer_ppd(self, uri): + """ + Discovery and installation of PPD driver (for legacy printers) + """ if not any(x in uri for x in self.SUPPORTED_PRINTERS): logger.error(f"Cannot install printer ppd for unsupported printer: {uri}") raise ExportException(sdstatus=Status.ERROR_PRINTER_NOT_SUPPORTED) @@ -230,8 +262,9 @@ def _install_printer_ppd(self, uri): return printer_ppd - def _setup_printer(self, printer_uri, printer_ppd): + def _setup_printer(self, printer_uri): # Add the printer using lpadmin + printer_ppd = self._install_printer_ppd(printer_uri) logger.info(f"Setting up printer {self.printer_name}") self.check_output_and_stderr( command=[ From 0bfba7e52a53caa9b700369813e439f6380b623a Mon Sep 17 00:00:00 2001 From: deeplow Date: Tue, 17 Dec 2024 12:01:23 +0000 Subject: [PATCH 2/5] Pull in pygobject as a Debian package dependency Necessary for GTK print dialog --- debian/control | 6 +++--- debian/setup-venv.sh | 2 +- export/poetry.lock | 37 +++++++++++++++++++++++++++++++++++-- export/pyproject.toml | 9 +++++++++ 4 files changed, 48 insertions(+), 6 deletions(-) diff --git a/debian/control b/debian/control index 03ecfff7c..0665922b2 100644 --- a/debian/control +++ b/debian/control @@ -13,10 +13,10 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, python3, python3-pyqt5, python3-pyq Description: securedrop client for qubes workstation Package: securedrop-export -Architecture: all +Architecture: amd64 Depends: ${misc:Depends}, python3, udisks2, cups, cups-ipp-utils, printer-driver-brlaser, printer-driver-hpcups, - avahi-daemon, system-config-printer, xpp, libcups2, gnome-disk-utility, libreoffice, - desktop-file-utils, shared-mime-info, libfile-mimeinfo-perl + avahi-daemon, system-config-printer, libcups2, gnome-disk-utility, libreoffice, + desktop-file-utils, shared-mime-info, libfile-mimeinfo-perl, gir1.2-gtk-4.0 Description: Submission export scripts for SecureDrop Workstation This package provides scripts used by the SecureDrop Qubes Workstation to export submissions from the client to external storage, via the sd-export diff --git a/debian/setup-venv.sh b/debian/setup-venv.sh index 3e8a01b47..b3cddb357 100644 --- a/debian/setup-venv.sh +++ b/debian/setup-venv.sh @@ -3,7 +3,7 @@ set -euxo pipefail NAME=$1 -if [[ $NAME == "client" ]]; then +if [[ $NAME == "client" || $NAME == "export" ]]; then VENV_ARGS="--system-site-packages" else VENV_ARGS="" diff --git a/export/poetry.lock b/export/poetry.lock index e3ca53171..ea39e7b81 100644 --- a/export/poetry.lock +++ b/export/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "attrs" @@ -755,6 +755,26 @@ files = [ {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, ] +[[package]] +name = "pycairo" +version = "1.20.1" +description = "Python interface for cairo" +optional = false +python-versions = ">=3.6, <4" +files = [ + {file = "pycairo-1.20.1-cp310-cp310-win32.whl", hash = "sha256:736ffc618e851601e861a630293e5c910ef016b83b2d035a336f83a367bf56ab"}, + {file = "pycairo-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:261c69850d4b2ec03346c9745bad2a835bb8124e4c6961b8ceac503d744eb3b3"}, + {file = "pycairo-1.20.1-cp36-cp36m-win32.whl", hash = "sha256:6db823a18e7be1eb2a29c28961f2f01e84d3b449f06be7338d05ac8f90592cd5"}, + {file = "pycairo-1.20.1-cp36-cp36m-win_amd64.whl", hash = "sha256:5525da2d8de912750dd157752aa96f1f0a42a437c5625e85b14c936b5c6305ae"}, + {file = "pycairo-1.20.1-cp37-cp37m-win32.whl", hash = "sha256:c8c2bb933974d91c5d19e54b846d964de177e7bf33433bf34ac34c85f9b30e94"}, + {file = "pycairo-1.20.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9a32e4a3574a104aa876c35d5e71485dfd6986b18d045534c6ec510c44d5d6a7"}, + {file = "pycairo-1.20.1-cp38-cp38-win32.whl", hash = "sha256:0d7a6754d410d911a46f00396bee4be96500ccd3d178e7e98aef1140e3dd67ae"}, + {file = "pycairo-1.20.1-cp38-cp38-win_amd64.whl", hash = "sha256:b605151cdd23cedb31855b8666371b6e26b80f02753a52c8b8023a916b1df812"}, + {file = "pycairo-1.20.1-cp39-cp39-win32.whl", hash = "sha256:e800486b51fffeb11ed867b4f2220d446e2a60a81a73b7c377123e0cbb72f49d"}, + {file = "pycairo-1.20.1-cp39-cp39-win_amd64.whl", hash = "sha256:f123d3818e30b77b7209d70a6dcfd5b4e34885f9fa539d92dd7ff3e4e2037213"}, + {file = "pycairo-1.20.1.tar.gz", hash = "sha256:1ee72b035b21a475e1ed648e26541b04e5d7e753d75ca79de8c583b25785531b"}, +] + [[package]] name = "pygments" version = "2.16.1" @@ -769,6 +789,19 @@ files = [ [package.extras] plugins = ["importlib-metadata"] +[[package]] +name = "pygobject" +version = "3.42.2" +description = "Python bindings for GObject Introspection" +optional = false +python-versions = ">=3.6, <4" +files = [ + {file = "PyGObject-3.42.2.tar.gz", hash = "sha256:21524cef33100c8fd59dc135948b703d79d303e368ce71fa60521cc971cd8aa7"}, +] + +[package.dependencies] +pycairo = ">=1.16,<2.0" + [[package]] name = "pytest" version = "8.3.3" @@ -1297,4 +1330,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "9f7314221516e99a4c6e35e2711e8e6367835291d2eeb520e4c141b3aa722c85" +content-hash = "5132d0fce724a362ce980de41881f5e2e9f65d69559d082e43116b55aca31a4e" diff --git a/export/pyproject.toml b/export/pyproject.toml index 92c3f6e53..74f285f38 100644 --- a/export/pyproject.toml +++ b/export/pyproject.toml @@ -11,6 +11,15 @@ python = "^3.11" pexpect = "^4.9.0" [tool.poetry.group.dev.dependencies] +# In production these two are installed using a system package +# so match those versions exactly +pygobject = [ + {version = "=3.42.2", python = ">=3.11"}, # bookworm +] +pycairo = [ + {version = "=1.20.1", python = ">=3.11"}, # bookworm +] + mypy = "^1.13.0" types-setuptools = "^75.2.0" pytest = "^8.3.3" From ffe386a460cff24e678b803d249878f397bcd16a Mon Sep 17 00:00:00 2001 From: deeplow Date: Mon, 13 Jan 2025 13:57:56 +0000 Subject: [PATCH 3/5] Replace XPP with Gtk print dialog and dependencies Note: system-wide dependencies made available from securedrop-export venv due to the need to access python3-gi. Otherwise it would require too many build dependencies and complexity. Given that python3-gi is already required by some Qubes components, it does not add too many new dependencies. --- .../securedrop_export/print/print_dialog.py | 50 +++++++++++++++++++ export/securedrop_export/print/service.py | 42 ++-------------- 2 files changed, 53 insertions(+), 39 deletions(-) create mode 100644 export/securedrop_export/print/print_dialog.py diff --git a/export/securedrop_export/print/print_dialog.py b/export/securedrop_export/print/print_dialog.py new file mode 100644 index 000000000..1242444f2 --- /dev/null +++ b/export/securedrop_export/print/print_dialog.py @@ -0,0 +1,50 @@ +import gi + +gi.require_version("Gtk", "4.0") +import logging + +from gi.repository import Gtk + +from securedrop_export.exceptions import ExportException +from securedrop_export.print.status import Status + +logger = logging.getLogger(__name__) + +def open_print_dialog(file_to_print): + app = PrintDialog(file_to_print) + app.run() + + +class PrintDialog(Gtk.Application): + def __init__(self, file_to_print): + super().__init__(application_id="org.securedrop.PrintDialog") + self.file_to_print = file_to_print + self.connect("activate", self.on_activate) + + def on_activate(self, app): + window = Gtk.Window(application=app) + self.dialog = Gtk.PrintUnixDialog.new("Print Document", window) + self.dialog.connect("response", self.on_response) + self.dialog.show() + window.hide() + + def on_response(self, parent_widget, response_id): + if response_id == Gtk.ResponseType.OK: + print(f"OK") + self.dialog.hide() + settings = self.dialog.get_settings() + printer = self.dialog.get_selected_printer() + page_setup = self.dialog.get_page_setup() + job = Gtk.PrintJob.new("print job", printer, settings, page_setup) + job.set_source_file(self.file_to_print) + job.send(self.on_job_complete, user_data=None) + elif response_id == Gtk.ResponseType.APPLY: # Preview (if available) + pass + elif response_id == Gtk.ResponseType.CANCEL: + # FIXME should this exist or should it simply cancel and not report errors + raise ExportException(sdstatus=Status.ERROR_PRINT, sderror="User canceled dialog") + + def on_job_complete(self, print_job, user_data, error): + if error: + self.quit() + raise ExportException(sdstatus=Status.ERROR_PRINT, sderror=error.message) diff --git a/export/securedrop_export/print/service.py b/export/securedrop_export/print/service.py index 7db747a75..35e55cad7 100644 --- a/export/securedrop_export/print/service.py +++ b/export/securedrop_export/print/service.py @@ -6,6 +6,7 @@ from securedrop_export.directory import safe_mkdir from securedrop_export.exceptions import ExportException +from .print_dialog import open_print_dialog from .status import Status logger = logging.getLogger(__name__) @@ -111,31 +112,6 @@ def printer_test(self) -> Status: # a success status here return Status.PRINT_TEST_PAGE_SUCCESS - def _wait_for_print(self): - """ - Use lpstat to ensure the job was fully transfered to the printer - Return True if print was successful, otherwise throw ExportException. - Currently, the handler `handler` is defined in `exceptions.py`. - """ - signal.signal(signal.SIGALRM, handler) - signal.alarm(self.printer_wait_timeout) - printer_idle_string = f"printer {self.printer_name} is idle" - while True: - try: - logger.info(f"Running lpstat waiting for printer {self.printer_name}") - output = subprocess.check_output(["lpstat", "-p", self.printer_name]) - if printer_idle_string in output.decode("utf-8"): - logger.info("Print completed") - return True - else: - time.sleep(5) - except subprocess.CalledProcessError: - raise ExportException(sdstatus=Status.ERROR_PRINT) - except TimeoutException: - logger.error(f"Timeout waiting for printer {self.printer_name}") - raise ExportException(sdstatus=Status.ERROR_PRINT) - return True - def _check_printer_setup(self) -> None: """ Check printer setup. @@ -423,20 +399,8 @@ def _print_file(self, file_to_print: Path): logger.error(f"Something went wrong: {file_to_print} not found") raise ExportException(sdstatus=Status.ERROR_PRINT) - logger.info(f"Sending file to printer {self.printer_name}") - try: - # We can switch to using libreoffice --pt $printer_cups_name - # here, and either print directly (headless) or use the GUI - subprocess.check_call( - ["xpp", "-P", self.printer_name, file_to_print], - ) - except subprocess.CalledProcessError as e: - raise ExportException(sdstatus=Status.ERROR_PRINT, sderror=e.output) - - # This is an addition to ensure that the entire print job is transferred over. - # If the job is not fully transferred within the timeout window, the user - # will see an error message. - self._wait_for_print() + logger.info("Opening print dialog") + open_print_dialog(str(file_to_print)) def check_output_and_stderr( self, command: str, error_status: Status, ignore_stderr_startswith=None From 3e72356a7a0c28ae757151bfe289a2cf9c59178f Mon Sep 17 00:00:00 2001 From: deeplow Date: Wed, 15 Jan 2025 14:15:00 +0000 Subject: [PATCH 4/5] WIP: Print Dialog: hack to obtain exceptions raised in print dialog Exceptions raised in GTK code are trapped. However, the print logic needs to know the result of the print dialog (usually communicated through exceptions). A context manager is used to keep track of the exception the GTK code would ideally raise. --- .../securedrop_export/print/print_dialog.py | 70 ++++++++++++++++--- 1 file changed, 59 insertions(+), 11 deletions(-) diff --git a/export/securedrop_export/print/print_dialog.py b/export/securedrop_export/print/print_dialog.py index 1242444f2..399ff5edb 100644 --- a/export/securedrop_export/print/print_dialog.py +++ b/export/securedrop_export/print/print_dialog.py @@ -10,27 +10,64 @@ logger = logging.getLogger(__name__) -def open_print_dialog(file_to_print): - app = PrintDialog(file_to_print) - app.run() + +class GtkExceptionRaiser: + """ + Context manager to keep track of exceptions to be raised after GTK exits + + This is a workaround for the fact that GTK does not behave like regular + libraries. Exceptions raised by the GUI code are always caught within GTK. + The context manager provides a way to store these exceptions. + + Usage: + + class SomeApplication(Gtk.Application): + def __init__(self, raise_later_func): + super().__init__() + self.raise_later_func = raise_later_func + + [...] + + def on_something_bad_happening(self): + self.raise_later_func(Exception("something happned")) + self.quit() + + with GtkExceptionRaiser() as raise_later_func: + app = SomeApplication(raise_later_func) + app.run() + """ + + def __init__(self): + self.exception_to_raise = None + + def raise_later_func(self, exception): + self.exception_to_raise = exception + + def __enter__(self): + return self.raise_later_func + + def __exit__(self, exc_type, exc_val, exc_tb): + if self.exception_to_raise: + raise self.exception_to_raise class PrintDialog(Gtk.Application): - def __init__(self, file_to_print): + def __init__(self, file_to_print, raise_later_func): super().__init__(application_id="org.securedrop.PrintDialog") self.file_to_print = file_to_print + self.raise_later_func = raise_later_func self.connect("activate", self.on_activate) def on_activate(self, app): window = Gtk.Window(application=app) + window.hide() self.dialog = Gtk.PrintUnixDialog.new("Print Document", window) self.dialog.connect("response", self.on_response) + self.dialog.connect("close", self.quit) self.dialog.show() - window.hide() def on_response(self, parent_widget, response_id): if response_id == Gtk.ResponseType.OK: - print(f"OK") self.dialog.hide() settings = self.dialog.get_settings() printer = self.dialog.get_selected_printer() @@ -38,13 +75,24 @@ def on_response(self, parent_widget, response_id): job = Gtk.PrintJob.new("print job", printer, settings, page_setup) job.set_source_file(self.file_to_print) job.send(self.on_job_complete, user_data=None) - elif response_id == Gtk.ResponseType.APPLY: # Preview (if available) - pass elif response_id == Gtk.ResponseType.CANCEL: # FIXME should this exist or should it simply cancel and not report errors - raise ExportException(sdstatus=Status.ERROR_PRINT, sderror="User canceled dialog") + self.raise_later_func( + ExportException(sdstatus=Status.ERROR_PRINT, sderror="User canceled dialog") + ) + self.quit() + elif response_id == Gtk.ResponseType.DELETE_EVENT: + self.quit() def on_job_complete(self, print_job, user_data, error): if error: - self.quit() - raise ExportException(sdstatus=Status.ERROR_PRINT, sderror=error.message) + self.raise_later_func( + ExportException(sdstatus=Status.ERROR_PRINT, sderror=error.message) + ) + self.quit() + + +def open_print_dialog(file_to_print): + with GtkExceptionRaiser() as raise_later_func: + app = PrintDialog(file_to_print, raise_later_func) + app.run() From 66eb2316652f0d770ce714f5741a1eaa0ae0e590 Mon Sep 17 00:00:00 2001 From: deeplow Date: Fri, 17 Jan 2025 11:35:25 +0000 Subject: [PATCH 5/5] Handle printer discovery failure through exceptions Incapsulate printer search and possible failure in the respective printer search methods. --- export/securedrop_export/print/exceptions.py | 9 ++++++ export/securedrop_export/print/service.py | 32 ++++++++++++-------- 2 files changed, 28 insertions(+), 13 deletions(-) create mode 100644 export/securedrop_export/print/exceptions.py diff --git a/export/securedrop_export/print/exceptions.py b/export/securedrop_export/print/exceptions.py new file mode 100644 index 000000000..da9e93843 --- /dev/null +++ b/export/securedrop_export/print/exceptions.py @@ -0,0 +1,9 @@ +from securedrop_export.exceptions import ExportException + +from .status import Status + + +class PrinterNotFoundException(ExportException): + def __init__(self, sderror=None): + super().__init__() + self.sdstatus = Status.ERROR_PRINTER_NOT_FOUND diff --git a/export/securedrop_export/print/service.py b/export/securedrop_export/print/service.py index 35e55cad7..b434fdf4a 100644 --- a/export/securedrop_export/print/service.py +++ b/export/securedrop_export/print/service.py @@ -6,6 +6,7 @@ from securedrop_export.directory import safe_mkdir from securedrop_export.exceptions import ExportException +from .exceptions import PrinterNotFoundException from .print_dialog import open_print_dialog from .status import Status @@ -120,9 +121,9 @@ def _check_printer_setup(self) -> None: legacy_printers = False logger.info("Searching for printer") - printers = self._get_printers_ipp() - if not printers: - # look for legacy printers after no IPP ones are detected + try: + printers = self._get_printers_ipp() + except PrinterNotFoundException: printers = self._get_printers_legacy() legacy_printers = True @@ -147,13 +148,18 @@ def _get_printers_legacy(self) -> list[str]: raise ExportException(sdstatus=Status.ERROR_UNKNOWN) discovered_printers = [x for x in output.decode("utf-8").split() if "usb://" in x] - supported_printers = [ p for p in discovered_printers if any(sub in p for sub in self.SUPPORTED_PRINTERS) ] + unsupported_printers = [p for p in discovered_printers if p not in supported_printers] + if not supported_printers: - logger.info(f"{discovered_printers} are unsupported printers") - raise ExportException(sdstatus=Status.ERROR_PRINTER_NOT_SUPPORTED) + if unsupported_printers: + logger.info(f"{', '.join(unsupported_printers)} are unsupported printers") + raise ExportException(sdstatus=Status.ERROR_PRINTER_NOT_SUPPORTED) + else: + logger.info("No legacy (PDP) printers were found") + raise PrinterNotFoundException() return supported_printers @@ -163,14 +169,14 @@ def _get_printers_ipp(self) -> list[str]: discovered_printers = subprocess.check_output( ["ippfind"], universal_newlines=True ).split() - except subprocess.CalledProcessError as e: - logger.error(e) - raise ExportException(sdstatus=Status.ERROR_UNKNOWN) - - if discovered_printers: logger.debug(f"Found IPP printers: {', '.join(discovered_printers)}") - else: - logger.debug("No IPP were found") + except subprocess.CalledProcessError as ex: + if ex.returncode == 1: # Did not find any match + logger.debug("No IPP printers were found") + raise PrinterNotFoundException() + else: + logger.error("'ippfind' command failed") + raise ExportException(sdstatus=Status.ERROR_PRINTER_URI) return discovered_printers