Is there a way to run a headless browser (with Playwright) from a background job?

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?