From 849785b668e4d48a15b8cd2c2acfffa51d29f773 Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Sat, 4 Apr 2026 09:15:46 +0530 Subject: [PATCH 1/2] feat: add UI debugging option --- frappe/utils/pdf_generator/browser.py | 14 ++++++++++---- frappe/utils/pdf_generator/chrome_pdf_generator.py | 5 ++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/frappe/utils/pdf_generator/browser.py b/frappe/utils/pdf_generator/browser.py index 7bc30ad9a5..80f8abd999 100644 --- a/frappe/utils/pdf_generator/browser.py +++ b/frappe/utils/pdf_generator/browser.py @@ -3,6 +3,7 @@ from typing import ClassVar from bs4 import BeautifulSoup import frappe +from frappe.utils.data import cint from frappe.utils.pdf import get_host_url from frappe.utils.print_utils import convert_uom, parse_float_and_unit @@ -10,6 +11,7 @@ from frappe.utils.print_utils import convert_uom, parse_float_and_unit class Browser: def __init__(self, generator, print_format, html, options): self.is_print_designer = frappe.get_cached_value("Print Format", print_format, "print_designer") + self.debug_mode = bool(cint(frappe.form_dict.get("pdf_debug"))) self.browserID = frappe.utils.random_string(10) generator.add_browser(self.browserID) # sets soup from html @@ -31,7 +33,8 @@ class Browser: # now wait for page to load as we need DOM to generate pdf self.body_page.wait_for_set_content() self.body_pdf = self.body_page.generate_pdf(raw=not self.header_page and not self.footer_page) - self.body_page.close() + if not self.debug_mode: + self.body_page.close() self.update_header_footer_page() if self.header_page: @@ -39,16 +42,19 @@ class Browser: self.header_pdf = self.header_page.get_pdf_from_stream(self.header_page.get_pdf_stream_id()) else: self.header_pdf = self.header_page.generate_pdf() - self.header_page.close() + if not self.debug_mode: + self.header_page.close() if self.footer_page: if not self.is_footer_dynamic: self.footer_pdf = self.footer_page.get_pdf_from_stream(self.footer_page.get_pdf_stream_id()) else: self.footer_pdf = self.footer_page.generate_pdf() - self.footer_page.close() + if not self.debug_mode: + self.footer_page.close() - self.close() + if not self.debug_mode: + self.close() generator.remove_browser(self.browserID) diff --git a/frappe/utils/pdf_generator/chrome_pdf_generator.py b/frappe/utils/pdf_generator/chrome_pdf_generator.py index 6973b503d3..5edbbae263 100644 --- a/frappe/utils/pdf_generator/chrome_pdf_generator.py +++ b/frappe/utils/pdf_generator/chrome_pdf_generator.py @@ -9,6 +9,7 @@ import requests import frappe from frappe import _ +from frappe.utils.data import cint from frappe.utils.print_utils import find_or_download_chromium_executable # TODO: close browser when worker is killed. @@ -69,11 +70,13 @@ class ChromePDFGenerator: self.USE_PERSISTENT_CHROMIUM = site_config.get("use_persistent_chromium", False) # time to wait for chromium to start and provide dev tools url used in _set_devtools_url. self.START_TIMEOUT = site_config.get("chromium_start_timeout", 3) + # Allow a single PDF request to opt into interactive Chromium debugging. + self.debug_mode = bool(cint(frappe.form_dict.get("pdf_debug"))) self._chromium_path = find_or_download_chromium_executable() if self._verify_chromium_installation(): if not self._devtools_url: - self.start_chromium_process() + self.start_chromium_process(debug=self.debug_mode) def _verify_chromium_installation(self): """Ensures Chromium is available and executable, raising clearer errors if not.""" From fcb40f71c4d5636ddef3ba9fb32a8fb7d59376e6 Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Sat, 4 Apr 2026 09:45:18 +0530 Subject: [PATCH 2/2] fix: restricts PDF debug mode to developer mode only --- frappe/utils/pdf_generator/browser.py | 4 +++- .../utils/pdf_generator/chrome_pdf_generator.py | 17 +++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/frappe/utils/pdf_generator/browser.py b/frappe/utils/pdf_generator/browser.py index 80f8abd999..93a137af2d 100644 --- a/frappe/utils/pdf_generator/browser.py +++ b/frappe/utils/pdf_generator/browser.py @@ -11,7 +11,7 @@ from frappe.utils.print_utils import convert_uom, parse_float_and_unit class Browser: def __init__(self, generator, print_format, html, options): self.is_print_designer = frappe.get_cached_value("Print Format", print_format, "print_designer") - self.debug_mode = bool(cint(frappe.form_dict.get("pdf_debug"))) + self.debug_mode = frappe.conf.developer_mode and bool(frappe.form_dict.get("pdf_debug")) self.browserID = frappe.utils.random_string(10) generator.add_browser(self.browserID) # sets soup from html @@ -57,6 +57,8 @@ class Browser: self.close() generator.remove_browser(self.browserID) + if self.debug_mode: + generator.detach_debug_browser() def open(self, generator): from frappe.utils.pdf_generator.cdp_connection import CDPSocketClient diff --git a/frappe/utils/pdf_generator/chrome_pdf_generator.py b/frappe/utils/pdf_generator/chrome_pdf_generator.py index 5edbbae263..41736f7934 100644 --- a/frappe/utils/pdf_generator/chrome_pdf_generator.py +++ b/frappe/utils/pdf_generator/chrome_pdf_generator.py @@ -70,8 +70,8 @@ class ChromePDFGenerator: self.USE_PERSISTENT_CHROMIUM = site_config.get("use_persistent_chromium", False) # time to wait for chromium to start and provide dev tools url used in _set_devtools_url. self.START_TIMEOUT = site_config.get("chromium_start_timeout", 3) - # Allow a single PDF request to opt into interactive Chromium debugging. - self.debug_mode = bool(cint(frappe.form_dict.get("pdf_debug"))) + # Allow a single PDF request to opt into interactive Chromium debugging in developer mode only. + self.debug_mode = frappe.conf.developer_mode and bool(frappe.form_dict.get("pdf_debug")) self._chromium_path = find_or_download_chromium_executable() if self._verify_chromium_installation(): @@ -234,6 +234,19 @@ class ChromePDFGenerator: self._devtools_url = None frappe.log("Headless Chromium closed successfully.") + def detach_debug_browser(self): + """ + Detach the generator from an interactive debug Chromium process. + + This keeps the debug browser window available for inspection, while ensuring + the next PDF request starts with a fresh generator/process instead of reusing + the old debug session. + """ + ChromePDFGenerator._instance = None + self._initialized = False + self._chromium_process = None + self._devtools_url = None + # not used anywhere in the code. read _set_devtools_url for more info. useful in case we want to take different approch to fetch devtools url. def fetch_devtools_url(self, port): if not port: