I have a very specific use case (Generating CAD previews) that requires using a headless browser.
I have succeeded to make it work by installing Playwright and their dependencies inside the Frappe environment:
bench pip install playwright
playwright install chromium
playwright install-deps chromium
# ...
dependencies = [
# "frappe~=15.0.0" # Installed and managed by bench.
"playwright~=1.54.0",
]
#...
Then writting functions like this:
from tempfile import NamedTemporaryFile
from pathlib import Path
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils.file_manager import get_file, save_file
from playwright.sync_api import sync_playwright
SHARECAD_UNWANTED_ELEMENTS = [".checkcookie", ".popup", ".noprint", ".header"]
def get_sharecad_screenshot(path: str | Path):
if not Path(path).exists():
frappe.throw(_("File not found: {0}").format(path))
screenshot_bytes = None
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page(record_video_dir="./recordings")
page.on("filechooser", lambda file_chooser: file_chooser.set_files(path))
page.goto("https://sharecad.org/")
page.locator(".text-file").click()
page.wait_for_load_state("networkidle")
page.add_style_tag(content=f"{', '.join(SHARECAD_UNWANTED_ELEMENTS)} {{display: none !important;}}")
screenshot_bytes = page.screenshot()
page.close()
return screenshot_bytes
class ManufacturingItem(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
from smris_management.smris_management.doctype.manufacturing_machine.manufacturing_machine import (
ManufacturingMachine,
)
amended_from: DF.Link | None
customer: DF.Link
design_files: DF.Attach
element: DF.Data
manufacturing_machines: DF.TableMultiSelect[ManufacturingMachine]
preview: DF.AttachImage | None
product: DF.Autocomplete
quantity: DF.Int
raw_material: DF.Autocomplete
thickness: DF.Float
# end: auto-generated types
pass
def before_save(self):
self.generate_preview()
def generate_preview(self):
"""Generate a preview image for the Manufacturing Item."""
if not self.design_files:
frappe.throw(_("No design file found for preview generation."))
design_file_path = Path(self.design_files)
with NamedTemporaryFile(
delete=False,
suffix=design_file_path.suffix,
prefix=design_file_path.stem + "_",
) as temp_file:
temp_file.write(get_file(self.design_files)[-1])
saved_file_path = Path(temp_file.name)
screenshot_bytes = get_sharecad_screenshot(saved_file_path)
file_doc = save_file(
f"preview_{self.name}.png",
screenshot_bytes,
"Manufacturing Item",
self.name,
decode=False, # Since we're passing bytes directly
is_private=1, # Set to 1 if you want the file to be private
)
self.preview = file_doc.file_url
saved_file_path.unlink()
The previous code works, but get_sharecad_screenshot
takes a few seconds to complete, blocking the UI and making the user experience in Frappe Desk bad.
I wanted to transform this into a background by changing before_save
like this:
def before_save(self):
frappe.enqueue_doc("Manufacturing Item", self.name, "generate_preview", queue="short")
Once I did that, it broke completely and shown errors like this in logs/worker.error.log
:
Traceback (most recent call last):
File "/workspace/frappe-bench/env/lib/python3.12/site-packages/rq/worker.py", line 1428, in perform_job
rv = job.perform()
^^^^^^^^^^^^^
File "/workspace/frappe-bench/env/lib/python3.12/site-packages/rq/job.py", line 1278, in perform
self._result = self._execute()
^^^^^^^^^^^^^^^
File "/workspace/frappe-bench/env/lib/python3.12/site-packages/rq/job.py", line 1315, in _execute
result = self.func(*self.args, **self.kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/workspace/frappe-bench/apps/frappe/frappe/utils/background_jobs.py", line 225, in execute_job
retval = method(**kwargs)
^^^^^^^^^^^^^^^^
File "/workspace/frappe-bench/apps/frappe/frappe/utils/background_jobs.py", line 190, in run_doc_method
getattr(frappe.get_doc(doctype, name), doc_method)(**kwargs)
File "/workspace/frappe-bench/apps/smris_management/smris_management/smris_management/doctype/manufacturing_item/manufacturing_item.py", line 92, in generate_preview
screenshot_bytes = get_sharecad_screenshot(saved_file_path)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/workspace/frappe-bench/apps/smris_management/smris_management/smris_management/browser.py", line 15, in get_sharecad_screenshot
browser = p.chromium.launch(headless=True)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/workspace/frappe-bench/env/lib/python3.12/site-packages/playwright/sync_api/_generated.py", line 14494, in launch
self._sync(
File "/workspace/frappe-bench/env/lib/python3.12/site-packages/playwright/_impl/_sync_base.py", line 115, in _sync
return task.result()
^^^^^^^^^^^^^
File "/workspace/frappe-bench/env/lib/python3.12/site-packages/playwright/_impl/_browser_type.py", line 98, in launch
await self._channel.send(
File "/workspace/frappe-bench/env/lib/python3.12/site-packages/playwright/_impl/_connection.py", line 69, in send
return await self._connection.wrap_api_call(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/workspace/frappe-bench/env/lib/python3.12/site-packages/playwright/_impl/_connection.py", line 558, in wrap_api_call
raise rewrite_error(error, f"{parsed_st['apiName']}: {error}") from None
playwright._impl._errors.TargetClosedError: BrowserType.launch: Target page, context or browser has been closed
Browser logs:
<launching> /workspace/.cache/ms-playwright/chromium_headless_shell-1181/chrome-linux/headless_shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-extensions --disable-features=AcceptCHFrame,AutoExpandDetailsElement,AvoidUnnecessaryBeforeUnloadCheckSync,CertificateTransparencyComponentUpdater,DestroyProfileOnBrowserClose,DialMediaRouteProvider,ExtensionManifestV2Disabled,GlobalMediaControls,HttpsUpgrades,ImprovedCookieControls,LazyFrameLoading,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --enable-automation --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/tmp/playwright_chromiumdev_profile-zGWkET --remote-debugging-pipe --no-startup-window
<launched> pid=488
[pid=488][err] /workspace/.cache/ms-playwright/chromium_headless_shell-1181/chrome-linux/headless_shell: error while loading shared libraries: libnspr4.so: cannot open shared object file: No such file or directory
Call log:
- <launching> /workspace/.cache/ms-playwright/chromium_headless_shell-1181/chrome-linux/headless_shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-extensions --disable-features=AcceptCHFrame,AutoExpandDetailsElement,AvoidUnnecessaryBeforeUnloadCheckSync,CertificateTransparencyComponentUpdater,DestroyProfileOnBrowserClose,DialMediaRouteProvider,ExtensionManifestV2Disabled,GlobalMediaControls,HttpsUpgrades,ImprovedCookieControls,LazyFrameLoading,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --enable-automation --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/tmp/playwright_chromiumdev_profile-zGWkET --remote-debugging-pipe --no-startup-window
- <launched> pid=488
- [pid=488][err] /workspace/.cache/ms-playwright/chromium_headless_shell-1181/chrome-linux/headless_shell: error while loading shared libraries: libnspr4.so: cannot open shared object file: No such file or directory
It looks like Playwright can’t launch Chromium from the background job because error while loading shared libraries: libnspr4.so: cannot open shared object file: No such file or directory
.
Is there any way to give full access to the environment to the background jobs so they can do the same thing that Frappe does from the document controllers?